[feat] add first working use of pydantic input validation
Browse files- events/payload_point2.json +2 -1
- requirements_dev.txt +1 -0
- src/__init__.py +3 -1
- src/app.py +70 -21
- src/utilities/constants.py +2 -0
- src/utilities/type_hints.py +10 -0
events/payload_point2.json
CHANGED
|
@@ -9,5 +9,6 @@
|
|
| 9 |
"label": 0
|
| 10 |
}],
|
| 11 |
"zoom": 10,
|
| 12 |
-
"source_type": "Satellite"
|
|
|
|
| 13 |
}
|
|
|
|
| 9 |
"label": 0
|
| 10 |
}],
|
| 11 |
"zoom": 10,
|
| 12 |
+
"source_type": "Satellite",
|
| 13 |
+
"debug": true
|
| 14 |
}
|
requirements_dev.txt
CHANGED
|
@@ -7,5 +7,6 @@ numpy
|
|
| 7 |
onnxruntime
|
| 8 |
opencv-python
|
| 9 |
pillow
|
|
|
|
| 10 |
rasterio
|
| 11 |
requests
|
|
|
|
| 7 |
onnxruntime
|
| 8 |
opencv-python
|
| 9 |
pillow
|
| 10 |
+
pydantic>=2.0.3
|
| 11 |
rasterio
|
| 12 |
requests
|
src/__init__.py
CHANGED
|
@@ -2,7 +2,9 @@ from aws_lambda_powertools import Logger
|
|
| 2 |
import os
|
| 3 |
from pathlib import Path
|
| 4 |
|
|
|
|
|
|
|
| 5 |
|
| 6 |
PROJECT_ROOT_FOLDER = Path(globals().get("__file__", "./_")).absolute().parent.parent
|
| 7 |
MODEL_FOLDER = Path(os.path.join(PROJECT_ROOT_FOLDER, "models"))
|
| 8 |
-
app_logger = Logger()
|
|
|
|
| 2 |
import os
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
+
from src.utilities.constants import SERVICE_NAME
|
| 6 |
+
|
| 7 |
|
| 8 |
PROJECT_ROOT_FOLDER = Path(globals().get("__file__", "./_")).absolute().parent.parent
|
| 9 |
MODEL_FOLDER = Path(os.path.join(PROJECT_ROOT_FOLDER, "models"))
|
| 10 |
+
app_logger = Logger(service=SERVICE_NAME)
|
src/app.py
CHANGED
|
@@ -1,18 +1,58 @@
|
|
| 1 |
import json
|
| 2 |
import time
|
| 3 |
from http import HTTPStatus
|
| 4 |
-
from typing import Dict
|
| 5 |
|
| 6 |
from aws_lambda_powertools.event_handler import content_types
|
| 7 |
from aws_lambda_powertools.utilities.typing import LambdaContext
|
|
|
|
| 8 |
|
| 9 |
from src import app_logger
|
| 10 |
from src.io.coordinates_pixel_conversion import get_latlng_to_pixel_coordinates
|
| 11 |
from src.prediction_api.predictors import samexporter_predict
|
| 12 |
-
from src.utilities.constants import CUSTOM_RESPONSE_MESSAGES
|
| 13 |
from src.utilities.utilities import base64_decode
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def get_response(status: int, start_time: float, request_id: str, response_body: Dict = None) -> str:
|
| 17 |
"""
|
| 18 |
Return a response for frontend clients.
|
|
@@ -42,7 +82,7 @@ def get_response(status: int, start_time: float, request_id: str, response_body:
|
|
| 42 |
return json.dumps(response)
|
| 43 |
|
| 44 |
|
| 45 |
-
def get_parsed_bbox_points(request_input:
|
| 46 |
app_logger.info(f"try to parsing input request {request_input}...")
|
| 47 |
bbox = request_input["bbox"]
|
| 48 |
app_logger.debug(f"request bbox: {type(bbox)}, value:{bbox}.")
|
|
@@ -67,7 +107,7 @@ def get_parsed_bbox_points(request_input: Dict) -> Dict:
|
|
| 67 |
raise ValueError("valid prompt type is only 'point'")
|
| 68 |
|
| 69 |
app_logger.debug(f"bbox => {bbox}.")
|
| 70 |
-
app_logger.debug(f'
|
| 71 |
|
| 72 |
app_logger.info(f"unpacking elaborated {request_input}...")
|
| 73 |
return {
|
|
@@ -85,23 +125,7 @@ def lambda_handler(event: dict, context: LambdaContext):
|
|
| 85 |
app_logger.info(f"event version: {event['version']}.")
|
| 86 |
|
| 87 |
try:
|
| 88 |
-
|
| 89 |
-
app_logger.info(f"context:{context}...")
|
| 90 |
-
|
| 91 |
-
try:
|
| 92 |
-
body = event["body"]
|
| 93 |
-
except Exception as e_constants1:
|
| 94 |
-
app_logger.error(f"e_constants1:{e_constants1}.")
|
| 95 |
-
body = event
|
| 96 |
-
|
| 97 |
-
app_logger.debug(f"body, #1: {type(body)}, {body}...")
|
| 98 |
-
|
| 99 |
-
if isinstance(body, str):
|
| 100 |
-
body_decoded_str = base64_decode(body)
|
| 101 |
-
app_logger.debug(f"body_decoded_str: {type(body_decoded_str)}, {body_decoded_str}...")
|
| 102 |
-
body = json.loads(body_decoded_str)
|
| 103 |
-
|
| 104 |
-
app_logger.info(f"body, #2: {type(body)}, {body}...")
|
| 105 |
|
| 106 |
try:
|
| 107 |
prompt_latlng = body["prompt"]
|
|
@@ -120,3 +144,28 @@ def lambda_handler(event: dict, context: LambdaContext):
|
|
| 120 |
|
| 121 |
app_logger.info(f"response_dumped:{response}...")
|
| 122 |
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import json
|
| 2 |
import time
|
| 3 |
from http import HTTPStatus
|
| 4 |
+
from typing import Dict, List
|
| 5 |
|
| 6 |
from aws_lambda_powertools.event_handler import content_types
|
| 7 |
from aws_lambda_powertools.utilities.typing import LambdaContext
|
| 8 |
+
from aws_lambda_powertools.utilities.parser import BaseModel
|
| 9 |
|
| 10 |
from src import app_logger
|
| 11 |
from src.io.coordinates_pixel_conversion import get_latlng_to_pixel_coordinates
|
| 12 |
from src.prediction_api.predictors import samexporter_predict
|
| 13 |
+
from src.utilities.constants import CUSTOM_RESPONSE_MESSAGES, DEFAULT_LOG_LEVEL
|
| 14 |
from src.utilities.utilities import base64_decode
|
| 15 |
|
| 16 |
|
| 17 |
+
list_float = List[float]
|
| 18 |
+
llist_float = List[list_float]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class LatLngDict(BaseModel):
|
| 22 |
+
lat: float
|
| 23 |
+
lng: float
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class RawBBox(BaseModel):
|
| 27 |
+
ne: LatLngDict
|
| 28 |
+
sw: LatLngDict
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class RawPrompt(BaseModel):
|
| 32 |
+
type: str
|
| 33 |
+
data: LatLngDict
|
| 34 |
+
label: int = 0
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class RawRequestInput(BaseModel):
|
| 38 |
+
bbox: RawBBox
|
| 39 |
+
prompt: RawPrompt
|
| 40 |
+
zoom: int | float
|
| 41 |
+
source_type: str = "Satellite"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ParsedPrompt(BaseModel):
|
| 45 |
+
type: str
|
| 46 |
+
data: llist_float
|
| 47 |
+
label: int = 0
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ParsedRequestInput(BaseModel):
|
| 51 |
+
bbox: llist_float
|
| 52 |
+
prompt: ParsedPrompt
|
| 53 |
+
zoom: int | float
|
| 54 |
+
|
| 55 |
+
|
| 56 |
def get_response(status: int, start_time: float, request_id: str, response_body: Dict = None) -> str:
|
| 57 |
"""
|
| 58 |
Return a response for frontend clients.
|
|
|
|
| 82 |
return json.dumps(response)
|
| 83 |
|
| 84 |
|
| 85 |
+
def get_parsed_bbox_points(request_input: RawRequestInput) -> Dict:
|
| 86 |
app_logger.info(f"try to parsing input request {request_input}...")
|
| 87 |
bbox = request_input["bbox"]
|
| 88 |
app_logger.debug(f"request bbox: {type(bbox)}, value:{bbox}.")
|
|
|
|
| 107 |
raise ValueError("valid prompt type is only 'point'")
|
| 108 |
|
| 109 |
app_logger.debug(f"bbox => {bbox}.")
|
| 110 |
+
app_logger.debug(f'request_input-prompt updated => {request_input["prompt"]}.')
|
| 111 |
|
| 112 |
app_logger.info(f"unpacking elaborated {request_input}...")
|
| 113 |
return {
|
|
|
|
| 125 |
app_logger.info(f"event version: {event['version']}.")
|
| 126 |
|
| 127 |
try:
|
| 128 |
+
body = get_parsed_request_body(context, event)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
try:
|
| 131 |
prompt_latlng = body["prompt"]
|
|
|
|
| 144 |
|
| 145 |
app_logger.info(f"response_dumped:{response}...")
|
| 146 |
return response
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_parsed_request_body(context, event):
|
| 150 |
+
app_logger.info(f"event:{json.dumps(event)}...")
|
| 151 |
+
app_logger.info(f"context:{context}...")
|
| 152 |
+
try:
|
| 153 |
+
body = event["body"]
|
| 154 |
+
except Exception as e_constants1:
|
| 155 |
+
app_logger.error(f"e_constants1:{e_constants1}.")
|
| 156 |
+
body = event
|
| 157 |
+
app_logger.debug(f"body, #1: {type(body)}, {body}...")
|
| 158 |
+
if isinstance(body, str):
|
| 159 |
+
body_decoded_str = base64_decode(body)
|
| 160 |
+
app_logger.debug(f"body_decoded_str: {type(body_decoded_str)}, {body_decoded_str}...")
|
| 161 |
+
body = json.loads(body_decoded_str)
|
| 162 |
+
app_logger.info(f"body, #2: {type(body)}, {body}...")
|
| 163 |
+
try:
|
| 164 |
+
log_level = 'DEBUG' if body['debug'] else DEFAULT_LOG_LEVEL
|
| 165 |
+
app_logger.warning(f"set logger level to DEBUG")
|
| 166 |
+
app_logger.setLevel(log_level)
|
| 167 |
+
except KeyError:
|
| 168 |
+
app_logger.warning(f"can't set log level, reset it...")
|
| 169 |
+
app_logger.setLevel(DEFAULT_LOG_LEVEL)
|
| 170 |
+
app_logger.warning(f"logger level is {app_logger.log_level}.")
|
| 171 |
+
return body
|
src/utilities/constants.py
CHANGED
|
@@ -16,3 +16,5 @@ WKT_3857 = 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",S
|
|
| 16 |
WKT_3857 += 'AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],'
|
| 17 |
WKT_3857 += 'PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4",'
|
| 18 |
WKT_3857 += '"+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]'
|
|
|
|
|
|
|
|
|
| 16 |
WKT_3857 += 'AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],'
|
| 17 |
WKT_3857 += 'PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4",'
|
| 18 |
WKT_3857 += '"+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]'
|
| 19 |
+
SERVICE_NAME = "sam-gis"
|
| 20 |
+
DEFAULT_LOG_LEVEL = 'INFO'
|
src/utilities/type_hints.py
CHANGED
|
@@ -1,3 +1,13 @@
|
|
| 1 |
"""custom type hints"""
|
|
|
|
|
|
|
|
|
|
| 2 |
ts_dict_str2 = dict[str, str]
|
| 3 |
ts_dict_str3 = dict[str, str, any]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""custom type hints"""
|
| 2 |
+
from typing import TypedDict
|
| 3 |
+
|
| 4 |
+
|
| 5 |
ts_dict_str2 = dict[str, str]
|
| 6 |
ts_dict_str3 = dict[str, str, any]
|
| 7 |
+
ts_ddict1 = dict[str, dict[str, any], dict, dict, any]
|
| 8 |
+
# ts_list_float = list[float, float]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class PixelCoordinate(TypedDict):
|
| 12 |
+
x: int
|
| 13 |
+
y: int
|