Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 6 additions & 22 deletions annotation_api/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@
from config import s3_config
from config import FlaskConfig, cache_config, s3_config

#---------------------------------------------------------------------------------------------------------------------------#

def create_app():
app = Flask(__name__)
app.config.from_object(FlaskConfig)

CORS(app, resources={
r'/api/*': {
'origins': [
app.config['ORIGIN_URL'],
],
'origins': app.config['ORIGIN_URLS'],
'supports_credentials': True
}
})

# Register s3 service app wide
setattr(app, 's3', client(
's3',
config=s3_config,
Expand All @@ -36,26 +38,8 @@ def create_app():
app.errorhandler(HTTPException)(errors.handle_generic_http)
app.errorhandler(500)(errors.internal_service_error)

from app.routes import bp
from app.routers import bp
app.register_blueprint(bp)

from app.routers.projects import projectBp
app.register_blueprint(projectBp)

from app.routers.models import modelBp
app.register_blueprint(modelBp)

from app.routers.images import imageBp
app.register_blueprint(imageBp)

from app.routers.surveys import surveyBp
app.register_blueprint(surveyBp)

from app.routers.herdunits import herdunitBp
app.register_blueprint(herdunitBp)

from app.routers.schemas import schemaBp
app.register_blueprint(schemaBp)

return app

17 changes: 17 additions & 0 deletions annotation_api/app/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from functools import wraps
from flask import abort
from flask_login import current_user

def roles_required(*roles):
def wrapper(f):
@wraps(f)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated:
return abort(401)

if not any(current_user.has_role(r) for r in roles):
return abort(403)

return f(*args, **kwargs)
return decorated_view
return wrapper
1 change: 1 addition & 0 deletions annotation_api/app/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .router import *
2 changes: 2 additions & 0 deletions annotation_api/app/routers/annotations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .annotation_validators import *
from .annotations import *
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions annotation_api/app/routers/autocropper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .autocropper_validators import *
from .autocropper import *
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions annotation_api/app/routers/cropverifier/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .cropverifier import *
from .cropverifier_validators import *
140 changes: 140 additions & 0 deletions annotation_api/app/routers/cropverifier/cropverifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Endpoints for crop verification in the API
# Author: Michael B. Lance

#---------------------------------------------------------------------------------------------------------------------------#

from uuid import UUID

from botocore.exceptions import ClientError
from flask import Blueprint, abort, request
from flask_login import login_required, current_user
from flask_pydantic import validate
from psycopg.errors import DatabaseError, UniqueViolation

from app.extensions import base, s3
from database import ObjectNotFound

from .cropverifier_validators import *

verifierBp = Blueprint('verifier', __name__, url_prefix='/api/v1/verifier')

#---------------------------------------------------------------------------------------------------------------------------#
# GET

@verifierBp.get('/reviewed-area')
@login_required
@validate()
def get(query: RAQuery):
'''
Retrieve a reviewed area
---
paramaters:
- in: query
name: herd_unit_id
type: number
- in: query
name: survey_id
type: number
responses:
200:
description: List of reviewed areas.
400:
description: Invalid UUID format.
404:
description: No reviewed areas found.
500:
description: Database error.
'''
try:
params = query.model_dump()
params['num'] = 1
reviewed_areas = base.get_crop_to_review(params, current_user.user_id)
except ObjectNotFound as e:
abort(404, str(e))
except (DatabaseError, Exception) as e:
print(e)
abort(500)

print(reviewed_areas)

return [ra.to_dict() for ra in reviewed_areas], 200

@verifierBp.get('/needing-reviewed')
@login_required
@validate()
def get_selection_count(query: RAQuery):
'''

'''
try:
count = base.get_crop_to_review_selection_count(query.model_dump())
except ObjectNotFound as e:
abort(404, str(e))
except (DatabaseError, Exception):
abort(500)

return {'count': count}, 200

#---------------------------------------------------------------------------------------------------------------------------#
# PUT

@verifierBp.put('/submit')
@login_required
@validate()
def approve_annotations(body: ApproveAnnotations):
'''

'''
data = body.model_dump()

# loop over incoming annotation data
for annot in data['annotations']:

# check if annotation in the database
try:
if base.get_annotation_exists(UUID(annot['uuid'])):

# TODO: check if annotation still inersects prediction in threshold
# update annotation
res_i = base.update_annotation(
annot['annotation_id'],
label_id=annot['label_id'],
box_tx=annot['dimensions']['top_left']['x'],
box_ty=annot['dimensions']['top_left']['y'],
box_bx=annot['dimensions']['bottom_right']['x'],
box_by=annot['dimensions']['bottom_right']['y'],
)
# else
else:
# create annotation
base.create_annotation(
label_id = annot['label_id'],
image_id = annot['image_id'],
herd_unit_id = annot['herd_unit_id'],
box_tx = annot['dimensions']['top_left']['x'],
box_ty = annot['dimensions']['top_left']['y'],
box_bx = annot['dimensions']['bottom_right']['x'],
box_by = annot['dimensions']['bottom_right']['y'],
user_id = current_user.user_id,
uuid = annot['uuid']
)
except ObjectNotFound as e:
abort(404, str(e))
except (DatabaseError, Exception):
abort(500)

# loop over deleted annotations
try:
# TODO: add method to delete multiple dicts in a single pass
for annot in data['deleted_annotations']:
base.delete_annotation(annot['annotation_id'])

# set crop reviewed
base.update_reviewed_area(data['reviewed_area_id'], reviewed_by_user_id = current_user.user_id)

# set image closed
base.update_image(data['image_id'], {'opened_by_user_id':0})
except (DatabaseError, Exception):
abort(500)

return '', 201
24 changes: 24 additions & 0 deletions annotation_api/app/routers/cropverifier/cropverifier_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pydantic import BaseModel, field_validator
from typing import Union, List, Optional, Dict
from uuid import UUID

class RAQuery(BaseModel):
herd_unit_id: Optional[List[int]]
survey_id: Optional[List[int]]
include_reviewed: Optional[bool] = False
include_opened: Optional[bool] = False

@field_validator('herd_unit_id', 'survey_id', mode='before')
@classmethod
def ensure_list(cls, value):
if isinstance(value, list):
return value
if value is None or value == "":
return []
return [value]

class ApproveAnnotations(BaseModel):
reviewed_area_id: Union[int, UUID]
image_id: Union[int, UUID]
annotations: List[Dict]
deleted_annotations: List[Dict]
6 changes: 3 additions & 3 deletions annotation_api/app/routers/herdunits/herdunits.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_by_id(herd_unit_id: str):
abort(404, f'Herd Unit with ID {herd_unit_id} was not found!')

else:
return herd_unit.serialize()
return herd_unit.to_dict()


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
Expand Down Expand Up @@ -79,7 +79,7 @@ def get_surveys(herd_unit_id: str):
except (DatabaseError, Exception) as e:
abort(500)

return [survey.serialize() for survey in surveys], 200
return [survey.to_dict() for survey in surveys], 200

#---------------------------------------------------------------------------------------------------------------------------#
# POST
Expand All @@ -97,4 +97,4 @@ def create(body: CreateHerdUnit):
print(e)
abort(500)

return herd_unit.serialize(), 201
return herd_unit.to_dict(), 201
1 change: 0 additions & 1 deletion annotation_api/app/routers/images/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# images/__init__.py
from .image_validators import *
from .images import *
Loading
Loading