1- from fastapi import Request , Body
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 APIRouter , Body , Depends , HTTPException , Request , Response , status
66from slugify import slugify
77from sqlalchemy import func , insert , select , update
88from sqlalchemy .exc import NoResultFound , NoSuchTableError
9+
910dotenv .load_dotenv ()
1011
12+ from datetime import datetime , timezone
13+ from typing import Optional
14+
1115from api .database import (
1216 get_async_session ,
1317 get_engine ,
1418 get_table ,
1519 patch_sources_sub_table ,
1620 select_sources_sub_table ,
1721)
22+ from api .models .field_site import (
23+ BeddingFacing ,
24+ FieldSite ,
25+ Location ,
26+ Observation ,
27+ Photo ,
28+ PlanarOrientation ,
29+ )
1830from api .query_parser import ParserException
1931from 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
2332
2433interchange_router = APIRouter (
2534 prefix = "/interchange-data" ,
2635 tags = ["interchange-data" ],
2736 responses = {404 : {"description" : "Not found" }},
2837)
2938
30- #helpers
39+ # helpers
40+
3141
3242def _parse_date_time (x : Optional [str ]) -> Optional [datetime ]:
3343 if not x :
@@ -40,6 +50,7 @@ def _parse_date_time(x: Optional[str]) -> Optional[datetime]:
4050 except Exception :
4151 return None
4252
53+
4354def _to_float (v ) -> Optional [float ]:
4455 try :
4556 if v is None :
@@ -48,7 +59,8 @@ def _to_float(v) -> Optional[float]:
4859 except Exception :
4960 return None
5061
51- #normalize and require lat/lngs
62+
63+ # normalize and require lat/lngs
5264def _valid_coords (lat , lng ) -> bool :
5365 try :
5466 lat = float (lat )
@@ -57,6 +69,7 @@ def _valid_coords(lat, lng) -> bool:
5769 return False
5870 return - 90.0 <= lat <= 90.0 and - 180.0 <= lng <= 180.0
5971
72+
6073def _first_planar_from_spot (props ) -> Optional [PlanarOrientation ]:
6174 """Find first planar orientation with numeric strike & dip in StraboSpot props."""
6275 orientation = props .get ("orientation_data" )
@@ -69,9 +82,12 @@ def _first_planar_from_spot(props) -> Optional[PlanarOrientation]:
6982 strike = _to_float (item .get ("strike" ))
7083 dip = _to_float (item .get ("dip" ))
7184 if strike is not None and dip is not None :
72- return PlanarOrientation (strike = strike , dip = dip , facing = BeddingFacing .upright )
85+ return PlanarOrientation (
86+ strike = strike , dip = dip , facing = BeddingFacing .upright
87+ )
7388 return None
7489
90+
7591def _first_planar_from_checkin (checkin ) -> Optional [PlanarOrientation ]:
7692 """Find first observation with numeric strike & dip in Rockd checkin."""
7793 obs = checkin .get ("observations" )
@@ -84,9 +100,12 @@ def _first_planar_from_checkin(checkin) -> Optional[PlanarOrientation]:
84100 strike = _to_float (orientation .get ("strike" ))
85101 dip = _to_float (orientation .get ("dip" ))
86102 if strike is not None and dip is not None :
87- return PlanarOrientation (strike = strike , dip = dip , facing = BeddingFacing .upright )
103+ return PlanarOrientation (
104+ strike = strike , dip = dip , facing = BeddingFacing .upright
105+ )
88106 return None
89107
108+
90109def _first_planar_from_fieldsite (fs : FieldSite ) -> Optional [PlanarOrientation ]:
91110 """Return first PlanarOrientation in FieldSite.observations."""
92111 for ob in fs .observations or []:
@@ -126,14 +145,16 @@ def spot_to_fieldsite(feat) -> FieldSite:
126145 url = f"rockd://photo/{ pid } " ,
127146 width = int (img .get ("width" , 0 ) or 0 ),
128147 height = int (img .get ("height" , 0 ) or 0 ),
129- checksum = ""
148+ checksum = "" ,
130149 )
131150 )
132151 observations : list [Observation ] = []
133152 planar = _first_planar_from_spot (props )
134153 if planar :
135154 observations .append (Observation (data = planar ))
136- created = _parse_date_time (props .get ("time" ) or props .get ("date" )) or datetime .now (timezone .utc )
155+ created = _parse_date_time (props .get ("time" ) or props .get ("date" )) or datetime .now (
156+ timezone .utc
157+ )
137158
138159 mt = props .get ("modified_timestamp" )
139160 if mt is not None :
@@ -154,8 +175,11 @@ def spot_to_fieldsite(feat) -> FieldSite:
154175 observations = observations ,
155176 )
156177
178+
157179@interchange_router .post ("/spot-to-fieldsite" )
158- def multiple_spot_to_fieldsite (feat : Union [dict , List [dict ]] = Body (...)) -> List [FieldSite ]:
180+ def multiple_spot_to_fieldsite (
181+ feat : Union [dict , List [dict ]] = Body (...)
182+ ) -> List [FieldSite ]:
159183 """
160184 Accept a single FeatureCollection or a list of FeatureCollections and
161185 return a FieldSite for each qualifying Point feature (non-image-basemap).
@@ -199,12 +223,16 @@ def fieldsite_to_checkin(fs: FieldSite) -> dict:
199223 d ["photo" ] = fs .photos [0 ].id
200224 planar = _first_planar_from_fieldsite (fs )
201225 if planar :
202- d ["observations" ] = [{"orientation" : {"strike" : float (planar .strike ), "dip" : float (planar .dip )}}]
226+ d ["observations" ] = [
227+ {"orientation" : {"strike" : float (planar .strike ), "dip" : float (planar .dip )}}
228+ ]
203229 return d
204230
205231
206232@interchange_router .post ("/fieldsite-to-checkin" )
207- def multiple_fieldsite_to_checkin (fieldsites : list [FieldSite ] = Body (...)) -> list [dict ]:
233+ def multiple_fieldsite_to_checkin (
234+ fieldsites : list [FieldSite ] = Body (...),
235+ ) -> list [dict ]:
208236 out : list [dict ] = []
209237 for fs in fieldsites :
210238 try :
@@ -213,11 +241,19 @@ def multiple_fieldsite_to_checkin(fieldsites: list[FieldSite] = Body(...)) -> li
213241 continue
214242 return out
215243
244+
216245@interchange_router .post ("/spot-to-checkin" )
217- async def spot_to_checkin (request : Request , spot : Union [dict , List [dict ]] = Body (...)) -> list [dict ]:
246+ async def spot_to_checkin (
247+ request : Request , spot : Union [dict , List [dict ]] = Body (...)
248+ ) -> list [dict ]:
218249 """Pipeline: Spot JSON (FeatureCollection[s]) or FieldSite list -> Checkin list."""
219250 # If it's already a list of FieldSite-like dicts (has 'location'), skip the first hop
220- if isinstance (spot , list ) and spot and isinstance (spot [0 ], dict ) and "location" in spot [0 ]:
251+ if (
252+ isinstance (spot , list )
253+ and spot
254+ and isinstance (spot [0 ], dict )
255+ and "location" in spot [0 ]
256+ ):
221257 fieldsites : List [FieldSite ] = spot # already FieldSite-shaped
222258 else :
223259 # Convert FeatureCollection (or list of them) -> FieldSite list
0 commit comments