[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
|