1- from fastapi import Request , Body , Query
2- from typing import Any , Iterable , Union , List
3- import httpx
1+ from typing import Any , Iterable , List , Union
2+
43import dotenv
5- from fastapi import APIRouter , Depends , HTTPException , Response , status
4+ import httpx
5+ from fastapi import (
6+ APIRouter ,
7+ Body ,
8+ Depends ,
9+ HTTPException ,
10+ Query ,
11+ Request ,
12+ Response ,
13+ status ,
14+ )
615from slugify import slugify
716from sqlalchemy import func , insert , select , update
817from sqlalchemy .exc import NoResultFound , NoSuchTableError
18+
919dotenv .load_dotenv ()
1020
21+ from datetime import datetime , timezone
22+ from typing import Optional
23+
1124from api .database import (
1225 get_async_session ,
1326 get_engine ,
1427 get_table ,
1528 patch_sources_sub_table ,
1629 select_sources_sub_table ,
1730)
31+ from api .models .field_site import (
32+ BeddingFacing ,
33+ FieldSite ,
34+ Location ,
35+ Observation ,
36+ Photo ,
37+ PlanarOrientation ,
38+ )
1839from api .query_parser import ParserException
1940from api .routes .security import has_access
20- from datetime import datetime , timezone
21- from api .models .field_site import FieldSite , Location , Photo , PlanarOrientation , Observation , BeddingFacing
22- from typing import Optional
2341
2442convert_router = APIRouter (
2543 prefix = "/convert" ,
2644 tags = ["convert" ],
2745 responses = {404 : {"description" : "Not found" }},
2846)
2947
30- #helpers
48+ # helpers
49+
3150
3251def _parse_date_time (x : Optional [str ]) -> Optional [datetime ]:
3352 if not x :
@@ -40,6 +59,7 @@ def _parse_date_time(x: Optional[str]) -> Optional[datetime]:
4059 except Exception :
4160 return None
4261
62+
4363def _to_float (v ) -> Optional [float ]:
4464 try :
4565 if v is None :
@@ -48,7 +68,8 @@ def _to_float(v) -> Optional[float]:
4868 except Exception :
4969 return None
5070
51- #normalize and require lat/lngs
71+
72+ # normalize and require lat/lngs
5273def _valid_coords (lat , lng ) -> bool :
5374 try :
5475 lat = float (lat )
@@ -57,6 +78,7 @@ def _valid_coords(lat, lng) -> bool:
5778 return False
5879 return - 90.0 <= lat <= 90.0 and - 180.0 <= lng <= 180.0
5980
81+
6082def _first_planar_from_spot (props ) -> Optional [PlanarOrientation ]:
6183 """Find first planar orientation with numeric strike & dip in StraboSpot props."""
6284 orientation = props .get ("orientation_data" )
@@ -69,9 +91,12 @@ def _first_planar_from_spot(props) -> Optional[PlanarOrientation]:
6991 strike = _to_float (item .get ("strike" ))
7092 dip = _to_float (item .get ("dip" ))
7193 if strike is not None and dip is not None :
72- return PlanarOrientation (strike = strike , dip = dip , facing = BeddingFacing .upright )
94+ return PlanarOrientation (
95+ strike = strike , dip = dip , facing = BeddingFacing .upright
96+ )
7397 return None
7498
99+
75100def _first_planar_from_checkin (checkin ) -> Optional [PlanarOrientation ]:
76101 """Find first observation with numeric strike & dip in Rockd checkin."""
77102 obs = checkin .get ("observations" )
@@ -84,9 +109,12 @@ def _first_planar_from_checkin(checkin) -> Optional[PlanarOrientation]:
84109 strike = _to_float (orientation .get ("strike" ))
85110 dip = _to_float (orientation .get ("dip" ))
86111 if strike is not None and dip is not None :
87- return PlanarOrientation (strike = strike , dip = dip , facing = BeddingFacing .upright )
112+ return PlanarOrientation (
113+ strike = strike , dip = dip , facing = BeddingFacing .upright
114+ )
88115 return None
89116
117+
90118def _first_planar_from_fieldsite (fs : FieldSite ) -> Optional [PlanarOrientation ]:
91119 """Return first PlanarOrientation in FieldSite.observations."""
92120 for ob in fs .observations or []:
@@ -126,14 +154,16 @@ def spot_to_fieldsite(feat) -> FieldSite:
126154 url = f"rockd://photo/{ pid } " ,
127155 width = int (img .get ("width" , 0 ) or 0 ),
128156 height = int (img .get ("height" , 0 ) or 0 ),
129- checksum = ""
157+ checksum = "" ,
130158 )
131159 )
132160 observations : list [Observation ] = []
133161 planar = _first_planar_from_spot (props )
134162 if planar :
135163 observations .append (Observation (data = planar ))
136- created = _parse_date_time (props .get ("time" ) or props .get ("date" )) or datetime .now (timezone .utc )
164+ created = _parse_date_time (props .get ("time" ) or props .get ("date" )) or datetime .now (
165+ timezone .utc
166+ )
137167
138168 mt = props .get ("modified_timestamp" )
139169 if mt is not None :
@@ -154,7 +184,10 @@ def spot_to_fieldsite(feat) -> FieldSite:
154184 observations = observations ,
155185 )
156186
157- def multiple_spot_to_fieldsite (feat : Union [dict , List [dict ]] = Body (...)) -> List [FieldSite ]:
187+
188+ def multiple_spot_to_fieldsite (
189+ feat : Union [dict , List [dict ]] = Body (...)
190+ ) -> List [FieldSite ]:
158191 """
159192 Accept a single FeatureCollection or a list of FeatureCollections and
160193 return a FieldSite for each qualifying Point feature.
@@ -198,11 +231,15 @@ def fieldsite_to_checkin(fs: FieldSite) -> dict:
198231 d ["photo" ] = fs .photos [0 ].id
199232 planar = _first_planar_from_fieldsite (fs )
200233 if planar :
201- d ["observations" ] = [{"orientation" : {"strike" : float (planar .strike ), "dip" : float (planar .dip )}}]
234+ d ["observations" ] = [
235+ {"orientation" : {"strike" : float (planar .strike ), "dip" : float (planar .dip )}}
236+ ]
202237 return d
203238
204239
205- def multiple_fieldsite_to_checkin (fieldsites : list [FieldSite ] = Body (...)) -> list [dict ]:
240+ def multiple_fieldsite_to_checkin (
241+ fieldsites : list [FieldSite ] = Body (...),
242+ ) -> list [dict ]:
206243 out : list [dict ] = []
207244 for fs in fieldsites :
208245 try :
@@ -213,10 +250,16 @@ def multiple_fieldsite_to_checkin(fieldsites: list[FieldSite] = Body(...)) -> li
213250 continue
214251 return out
215252
253+
216254def spot_to_checkin (spot : Union [dict , List [dict ]] = Body (...)) -> list [dict ]:
217255 """Pipeline: Spot JSON (FeatureCollection[s]) or FieldSite list -> Checkin list."""
218256 # If it's already a list of FieldSite-like dicts (has 'location'), skip the first hop
219- if isinstance (spot , list ) and spot and isinstance (spot [0 ], dict ) and "location" in spot [0 ]:
257+ if (
258+ isinstance (spot , list )
259+ and spot
260+ and isinstance (spot [0 ], dict )
261+ and "location" in spot [0 ]
262+ ):
220263 fieldsites : List [FieldSite ] = spot # already FieldSite-shaped
221264 else :
222265 # Convert FeatureCollection (or list of them) -> FieldSite list
@@ -225,7 +268,6 @@ def spot_to_checkin(spot: Union[dict, List[dict]] = Body(...)) -> list[dict]:
225268 return multiple_fieldsite_to_checkin (fieldsites )
226269
227270
228-
229271@convert_router .post ("/field-site" )
230272async def convert_field_site (
231273 payload : Union [dict , List [dict ]] = Body (...),
@@ -245,4 +287,7 @@ async def convert_field_site(
245287 return multiple_fieldsite_to_checkin (payload )
246288 if key == ("spot" , "checkin" ):
247289 return spot_to_checkin (payload )
248- raise HTTPException (status_code = 400 , detail = "Unsupported conversion. Use in=[spot|fieldsite], out=[fieldsite|checkin]." )
290+ raise HTTPException (
291+ status_code = 400 ,
292+ detail = "Unsupported conversion. Use in=[spot|fieldsite], out=[fieldsite|checkin]." ,
293+ )
0 commit comments