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
14 changes: 10 additions & 4 deletions annotation_api/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import os
from config import s3_config

from boto3 import client
from flasgger import Swagger
from flask import Flask
from flask_cors import CORS
import app.errors as errors
from werkzeug.exceptions import HTTPException
from config import FlaskConfig, s3_config, cache_config
from app.extensions import login_manager, cache, session_manager, base

import app.errors as errors
from app.extensions import base, cache, login_manager, session_manager
from config import s3_config
from config import FlaskConfig, cache_config, s3_config


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

CORS(app, resources={
r'/api/*': {
'origins': [
Expand Down
13 changes: 13 additions & 0 deletions annotation_api/app/routers/images/image_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ class CreateImage(BaseModel):
has_detection: bool = False
dem_name: Optional[str] = None
bbox_wsen: Optional[List[int]] = None

class UpdateImage(BaseModel):
name: Optional[str] = None
herd_unit_id: Optional[int] = None
survey_id: Optional[int] = None
img_key: Optional[str] = None
image_length_px: Optional[int] = None
image_width_px: Optional[int] = None
area: Optional[int] = None
viewshed_polygon: Optional[List[List[float]]] = None
has_detection: bool = False
dem_name: Optional[str] = None
bbox_wsen: Optional[List[int]] = None
107 changes: 92 additions & 15 deletions annotation_api/app/routers/images/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#---------------------------------------------------------------------------------------------------------------------------#

from flask import Blueprint, abort, request, current_app
from .image_validators import CreateImage
from .image_validators import CreateImage, UpdateImage
from app.extensions import base, s3
from botocore.exceptions import ClientError
from flask_pydantic import validate
Expand Down Expand Up @@ -35,16 +35,22 @@ def get_all():

@imageBp.get('/<string:image_id>')
@login_required
def get_by_id(body: CreateImage, image_id: str):
"""
Test Endpoint
def get_by_id(image_id: str):
'''
Request an image object from the database using its UUID
---
parameters:
- name: image_id
in: path
type: string
required: true

responses:
200:
description: A valid response # Must be indented under 200
404:
description: Not found
"""
200:
description: The requested image was found
404:
description: Not found
'''
image = base.get_image(UUID(image_id))

if image is not None:
Expand All @@ -56,6 +62,48 @@ def get_by_id(body: CreateImage, image_id: str):

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@imageBp.get('/<string:image_id>/crops')
@login_required
def get_crops(image_id: str):
'''

'''
crops = base.get_image_crops(UUID(image_id))

if len(crops) == 0:
abort(404, 'No crops found')

return [crop.serialize() for crop in crops], 200

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@imageBp.get('/<string:image_id>/predictions')
@login_required
def get_predictions(image_id: str):
'''

'''
predictions = base.get_image_predictions(UUID(image_id))

if len(predictions) == 0:
abort(404, 'No predictions found')

return [pred.serialize() for pred in predictions], 200

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@imageBp.get('/<string:image_id>/annotations')
@login_required
def get_annotations(image_id: str):
'''
'''
annotations = base.get_image_annotations(UUID(image_id))

if len(annotations) == 0:
abort(404, 'No annotations found')

return [annot.serialize() for annot in annotations], 200

#---------------------------------------------------------------------------------------------------------------------------#
#POST

Expand All @@ -76,21 +124,20 @@ def create(body: CreateImage):

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@imageBp.post('/presigned_url')
@imageBp.post('/<string:image_id>/presigned_url')
@login_required
def create_image_presigned_get():
def create_presigned_get(image_id: str):
'''
'''
data = request.get_json()

print(data['ra_key'])
image = base.get_image(UUID(image_id))

try:
response = s3.generate_presigned_url(
'get_object',
Params = {
'Bucket': current_app.config['BUCKET_NAME'],
'Key': data['ra_key']
'Key': image.img_key
},
ExpiresIn = data['expires_in']
)
Expand All @@ -104,5 +151,35 @@ def create_image_presigned_get():
#---------------------------------------------------------------------------------------------------------------------------#
#PATCH

@imageBp.patch('/<string:image_id>')
@validate()
@login_required
def update(body: UpdateImage, image_id: str):
'''

'''
data = request.get_json()
try:
image = base.update_image(UUID(image_id), data)
except:
abort(500)

return image.serialize(), 200

#---------------------------------------------------------------------------------------------------------------------------#
#DELETE
#DELETE

@imageBp.delete('/<string:image_id>')
@login_required
def delete_image(image_id: str):
'''

'''
try:
res = base.delete_image(UUID(image_id))
except:
abort(500)
if res:
return '', 204
else:
abort(404, 'Could not find the image to delete')
2 changes: 2 additions & 0 deletions annotation_api/app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def unathorizated_callback():

@bp.route('/api/v1/authenticate', methods=['POST'])
def authenticate():
'''
'''
req_data = request.get_json()
if not req_data or 'external-id' not in req_data:
abort(400, 'malformed request')
Expand Down
6 changes: 6 additions & 0 deletions annotation_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ class FlaskConfig:
SESSION_REDIS = redis.from_url(os.environ.get('SESSION_REDIS'))
ORIGIN_URL = os.environ.get('ORIGIN_URL')
BUCKET_NAME = os.environ.get('BUCKET_NAME')
SWAGGER = {
'title': 'AIrial API',
'version': '1.0.0',
'description': 'Note: the try it out button wont work because of auth. Will be fixed with oauth2.0',
'uiversion': 3,
}

db_config = {
'dbname': os.environ.get('DB_NAME'),
Expand Down
120 changes: 113 additions & 7 deletions annotation_api/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from datetime import datetime, date
from functools import wraps
import os
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
from typing import Any, Callable, Dict, List, Optional, Reversible, Tuple, Union, cast
from uuid import UUID
import uuid

Expand Down Expand Up @@ -1463,12 +1463,90 @@ def get_image(self, image_id: int | UUID) -> Image:
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@connect
def _update_image(self, cursor: psycopg.Cursor, image_id: int | UUID, parameters) -> bool:
''' Not fully implemented, do
def _get_image_crops(self, cursor: psycopg.Cursor[ReviewedArea], image_id: int | UUID) -> List[ReviewedArea]:
'''

'''
cursor.row_factory = class_row(ReviewedArea)
query = sql.SQL(' SELECT * FROM core.reviewed_area WHERE image_id = %s; ')

match image_id:
case int():
cursor.execute(query, (image_id,))
case UUID():
db_id = self.get_image(image_id).image_id
cursor.execute(query, (db_id,))
case _:
raise TypeError('image_id must be an integer, or UUID!')

return cursor.fetchall()

def get_image_crops(self, image_id: int | UUID) -> List[ReviewedArea]:
'''
'''
return self._get_image_crops(image_id)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@connect
def _get_image_predictions(self, cursor: psycopg.Cursor[Prediction], image_id: int | UUID) -> List[Prediction]:
'''
'''
cursor.row_factory = class_row(Prediction)
query = sql.SQL(' SELECT * FROM core.predictions WHERE image_id = %s; ')

match image_id:
case int():
cursor.execute(query, (image_id,))
case UUID():
db_id = self.get_image(image_id).image_id
cursor.execute(query, (db_id,))
case _:
raise TypeErorr('image_id must be an integer, or UUID!')

return cursor.fetchall()

def get_image_predictions(self, image_id: int | UUID) -> List[Prediction]:
'''
'''
return self._get_image_predictions(image_id = image_id)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@connect
def _get_image_annotations(self, cursor: psycopg.Cursor[Annotation], image_id: int | UUID) -> List[Annotation]:
'''
'''
cursor.row_factory = class_row(Annotation)
query = sql.SQL(' SELECT * FROM core.annotations WHERE image_id = %s; ')

match image_id:
case int():
cursor.execute(query, (image_id,))
case UUID():
db_id = self.get_image(image_id).image_id
cursor.execute((query), (db_id,))
case _:
raise TypeError('image_id must be an integer, or UUID!')

return cursor.fetchall()

def get_image_annotations(self, image_id: int | UUID) -> List[Annotation]:
'''
'''
return self._get_image_annotations(image_id = image_id)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@connect
def _update_image(self, cursor: psycopg.Cursor[Image], image_id: int | UUID, parameters) -> Image:
'''

'''
cursor.row_factory = class_row(Image)
query = sql.SQL(''' UPDATE core.images SET {augmented_field}, modified = CURRENT_TIMESTAMP
WHERE {id_field} = %s; ''')
WHERE {id_field} = %s
RETURNING *; ''')
kw_augmented_field = sql.SQL(',').join(
[
sql.SQL("{} = '%s'" % (value)).format(sql.Identifier(key))
Expand All @@ -1480,7 +1558,6 @@ def _update_image(self, cursor: psycopg.Cursor, image_id: int | UUID, parameters
and value is not None
]
)
print(kw_augmented_field.as_string(cursor))
match image_id:
case int():
cursor.execute(query.format(
Expand All @@ -1494,14 +1571,43 @@ def _update_image(self, cursor: psycopg.Cursor, image_id: int | UUID, parameters
), (image_id,))
case _:
raise TypeError('image_id must be an integer, or UUID')
return True
image = cursor.fetchone()

if image is None:
raise Exception('Failed to update image!')

return image

def update_image(self, image_id: int | UUID, parameters: dict) -> bool:
def update_image(self, image_id: int | UUID, parameters: dict) -> Image:
'''

'''
return self._update_image(image_id, parameters)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

@connect
def _delete_image(self, cursor: psycopg.Cursor, image_id: int | UUID) -> bool:
'''

'''
query = sql.SQL(''' DELETE FROM core.images WHERE {id_field} = %s; ''')
match image_id:
case int():
cursor.execute(query.format(id_field = sql.Identifier('image_id')), (image_id,))
case UUID():
cursor.execute(query.format(id_field = sql.Identifier('uuid')), (image_id,))
case _:
raise TypeError('image_id must be an integer, or UUID')

return True if cursor.rowcount > 0 else False

def delete_image(self, image_id: int | UUID) -> bool:
'''

'''
return self._delete_image(image_id=image_id)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# Core - Predictions

Expand Down
2 changes: 2 additions & 0 deletions development.compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ services:
test: valkey-cli ping || exit 1
restart: always
command: valkey-server --requirepass ${VALKEY_PASS}
ports:
- "6379:6379"
networks:
- dev_pronghorn_app_network
web-app-dev:
Expand Down
Binary file modified local_db/dev-bootstrap/3714.dat.gz
Binary file not shown.
Binary file modified local_db/dev-bootstrap/3716.dat.gz
Binary file not shown.
Binary file modified local_db/dev-bootstrap/3718.dat.gz
Binary file not shown.
Binary file modified local_db/dev-bootstrap/3720.dat.gz
Binary file not shown.
Binary file modified local_db/dev-bootstrap/3724.dat.gz
Binary file not shown.
Binary file modified local_db/dev-bootstrap/3726.dat.gz
Binary file not shown.
Binary file modified local_db/dev-bootstrap/toc.dat
Binary file not shown.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ contourpy==1.3.2
cycler==0.12.1
dnspython==2.8.0
email-validator==2.3.0
flasgger==0.9.7.1
flask==3.1.1
flask-caching==2.3.1
flask-cors==6.0.0
Expand Down
Loading
Loading