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
190 changes: 74 additions & 116 deletions api/geothermal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,126 +13,84 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
"""Geothermal well endpoints.

TEMPORARY BACKING: routes read from the legacy NM_Wells staging mirror
(``db/nmw_legacy.py``) via ``services/geothermal_helper.py``. Once the
NM_Wells -> Ocotillo transform lands these will be backed by the ``thing``
table and ``thing_id`` will be populated on the response. The route path lives
under ``/thing`` so the URL is stable across that swap.
"""

from typing import Optional
from uuid import UUID

from fastapi import APIRouter
from fastapi_pagination.ext.sqlalchemy import paginate
from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND

#
# from db.geothermal import (
# GeothermalTemperatureProfile,
# GeothermalTemperatureProfileObservation,
# GeothermalBottomHoleTemperature,
# GeothermalWellInterval,
# GeothermalHeatFlow,
# GeothermalThermalConductivity,
# GeothermalSampleSet,
# GeothermalBottomHoleTemperatureHeader,
# )
from api.pagination import CustomPage
from core.dependencies import session_dependency, viewer_dependency
from schemas.geothermal import GeothermalWellResponse
from services.exceptions_helper import PydanticStyleException
from services.geothermal_helper import (
geothermal_wells_transformer,
get_geothermal_well_by_id,
get_geothermal_wells_query,
)

router = APIRouter(prefix="/geothermal", tags=["geothermal"])
router = APIRouter(prefix="/thing", tags=["geothermal"])


# @router.post("/sample_set", status_code=status.HTTP_201_CREATED)
# async def add_geothermal_sample_set(
# sample_set_data: CreateGeothermalSampleSet, # Replace with appropriate schema
# session: session_dependency
# ):
# """
# Add a new geothermal sample set.
# """
# # Assuming you have a model for GeothermalSampleSet
# return adder(session, GeothermalSampleSet, sample_set_data)
#
#
# @router.post("/bottom_hole_temperature_header", status_code=status.HTTP_201_CREATED)
# async def add_bottom_hole_temperature_header(
# bottom_hole_temperature_header_data: CreateBottomHoleTemperatureHeader,
# session: session_dependency
# ):
# """
# Add a new bottom hole temperature header.
# """
# # Assuming you have a model for GeothermalBottomHoleTemperatureHeader
# return adder(
# session,
# GeothermalBottomHoleTemperatureHeader,
# bottom_hole_temperature_header_data,
# )
#
#
# @router.post("/temperature_profile", status_code=status.HTTP_201_CREATED)
# async def add_temperature_profile(
# temperature_profile_data: CreateTemperatureProfile,
# session: session_dependency
# ):
# """
# Add a new temperature profile.
# """
# return adder(session, GeothermalTemperatureProfile, temperature_profile_data)
#
#
# @router.post("/temperature_profile_observation", status_code=status.HTTP_201_CREATED)
# async def add_temperature_profile_observation(
# temperature_profile_observation_data: CreateTemperatureProfileObservation,
# session: session_dependency
# ):
# """
# Add a new temperature profile observation.
# """
# return adder(
# session,
# GeothermalTemperatureProfileObservation,
# temperature_profile_observation_data,
# )
#
#
# @router.post("/bottom_hole_temperature", status_code=status.HTTP_201_CREATED)
# async def add_bottom_hole_temperature(
# bottom_hole_temperature_data: CreateBottomHoleTemperature,
# session: session_dependency
# ):
# """
# Add a new bottom hole temperature.
# """
# return adder(
# session,
# GeothermalBottomHoleTemperature, # Assuming this is the correct model
# bottom_hole_temperature_data,
# )
#
#
# @router.post("/interval", status_code=status.HTTP_201_CREATED)
# async def add_geothermal_interval(
# interval_data: CreateGeothermalInterval, # Replace with appropriate schema
# session: session_dependency
# ):
# """
# Add a new geothermal interval.
# """
# # Assuming you have a model for GeothermalInterval
# return adder(session, GeothermalWellInterval, interval_data)
#
#
# @router.post("/thermal_conductivity", status_code=status.HTTP_201_CREATED)
# async def add_thermal_conductivity(
# thermal_conductivity_data: CreateThermalConductivity, # Replace with appropriate schema
# session: session_dependency
# ):
# """
# Add a new geothermal thermal conductivity.
# """
# # Assuming you have a model for GeothermalThermalConductivity
# return adder(session, GeothermalThermalConductivity, thermal_conductivity_data)
#
#
# @router.post("/heat_flow", status_code=status.HTTP_201_CREATED)
# async def add_heat_flow(
# heat_flow_data: CreateHeatFlow,
# session: session_dependency
# ):
# """
# Add a new geothermal heat flow.
# """
# # Assuming you have a model for GeothermalHeatFlow
# return adder(session, GeothermalHeatFlow, heat_flow_data)
#
@router.get(
"/geothermal-well",
summary="Get all geothermal wells",
status_code=HTTP_200_OK,
)
def get_geothermal_wells(
user: viewer_dependency,
session: session_dependency,
county: Optional[str] = None,
name_contains: Optional[str] = None,
) -> CustomPage[GeothermalWellResponse]:
"""List geothermal wells.

NOTE: sourced from the legacy NM_Wells mirror (NMW_WellHeaders where
GthrmExist is set). Will be re-pointed at the thing table post-transform.
"""
sql = get_geothermal_wells_query(county=county, name_contains=name_contains)
return paginate(query=sql, conn=session, transformer=geothermal_wells_transformer)


@router.get(
"/geothermal-well/{well_data_id}",
summary="Get geothermal well by legacy WellDataID",
status_code=HTTP_200_OK,
)
def get_geothermal_well(
user: viewer_dependency,
well_data_id: UUID,
session: session_dependency,
) -> GeothermalWellResponse:
"""Get a single geothermal well by its legacy NMW WellDataID (GUID).

NOTE: keyed by the legacy GUID because these rows are not yet in the thing
table. Post-transform this becomes an integer thing_id lookup.
"""
well = get_geothermal_well_by_id(session, well_data_id)
if well is None:
raise PydanticStyleException(
status_code=HTTP_404_NOT_FOUND,
detail=[
{
"loc": ["path", "well_data_id"],
"msg": f"Geothermal well with WellDataID {well_data_id} not found.",
"type": "value_error",
"input": {"well_data_id": str(well_data_id)},
}
],
)
return well


# ============= EOF =============================================
4 changes: 4 additions & 0 deletions core/initializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def register_api_routes(app):
from api.ngwmn import router as ngwmn_router
from api.feedback import router as feedback_router
from api.disclaimer import router as disclaimer_router
from api.geothermal import router as geothermal_router

app.include_router(asset_router)
app.include_router(author_router)
Expand All @@ -230,6 +231,9 @@ def register_api_routes(app):
app.include_router(sample_router)
app.include_router(sensor_router)
app.include_router(search_router)
# geothermal shares the /thing prefix; register before thing_router so its
# explicit /thing/geothermal-well routes take precedence over /thing/{id}
app.include_router(geothermal_router)
app.include_router(thing_router)
app.include_router(ngwmn_router)
app.include_router(feedback_router)
Expand Down
39 changes: 38 additions & 1 deletion schemas/geothermal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,44 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from pydantic import BaseModel
from datetime import datetime
from uuid import UUID

from pydantic import BaseModel, ConfigDict


class GeothermalWellResponse(BaseModel):
"""Read model for a geothermal well sourced from the legacy NM_Wells mirror.

NOTE: This currently reads directly from the ``NMW_WellHeaders`` /
``NMW_WellLocations`` staging tables (see ``db/nmw_legacy.py``). Once the
NM_Wells -> Ocotillo transform lands, these rows will be backed by the
``thing`` table and ``thing_id`` will be populated. Until then ``thing_id``
is always ``None`` and ``well_data_id`` (legacy GUID) is the identifier.
"""

model_config = ConfigDict(from_attributes=True)

well_data_id: UUID # legacy NMW_WellHeaders.WellDataID
thing_id: int | None = None # populated after NM_Wells -> thing transform

api: str | None = None
name: str | None = None # cur_well_nam
well_number: str | None = None # cur_well_num
well_class: str | None = None
well_type: str | None = None
status: str | None = None # cur_status
operator: str | None = None # cur_operatr
owner: str | None = None # cur_owner
total_depth: float | None = None
completion_date: datetime | None = None # compl_date
has_geothermal_data: bool | None = None # gthrm_exist

# location, joined from NMW_WellLocations on WellDataID
county: str | None = None
state: str | None = None
latitude: float | None = None # lat_dd83
longitude: float | None = None # long_dd83


class CreateTemperatureProfile(BaseModel):
Expand Down
99 changes: 99 additions & 0 deletions services/geothermal_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ===============================================================================
# Copyright 2026 ross
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
"""Read-side helpers for geothermal wells.

TEMPORARY SOURCE: these read straight from the legacy NM_Wells staging mirror
(``db/nmw_legacy.py``). A "geothermal well" is a ``NMW_WellHeaders`` row whose
``GthrmExist`` flag is set. Location (lat/long/county/state) is joined from
``NMW_WellLocations`` on ``WellDataID``.

Once the NM_Wells -> Ocotillo transform exists, swap the source to the ``thing``
table and populate ``thing_id`` on the response. Keeping the DB access behind
this helper is what makes that swap a one-file change.
"""

from uuid import UUID

from sqlalchemy import select

from db.nmw_legacy import NMW_WellHeaders, NMW_WellLocations
from schemas.geothermal import GeothermalWellResponse


def _base_query():
"""Header rows flagged geothermal, left-joined to their location."""
return (
select(NMW_WellHeaders, NMW_WellLocations)
.outerjoin(
NMW_WellLocations,
NMW_WellHeaders.well_data_id == NMW_WellLocations.well_data_id,
)
.where(NMW_WellHeaders.gthrm_exist == 1)
)


def _to_response(header: NMW_WellHeaders, location: NMW_WellLocations | None):
return GeothermalWellResponse(
well_data_id=header.well_data_id,
thing_id=None, # not yet linked; see NM_Wells -> thing transform
api=header.api,
name=header.cur_well_nam,
well_number=header.cur_well_num,
well_class=header.well_class,
well_type=header.well_type,
status=header.cur_status,
operator=header.cur_operatr,
owner=header.cur_owner,
total_depth=header.total_depth,
completion_date=header.compl_date,
has_geothermal_data=bool(header.gthrm_exist),
county=location.county if location else None,
state=location.state if location else None,
latitude=location.lat_dd83 if location else None,
longitude=location.long_dd83 if location else None,
)


def get_geothermal_wells_query(
county: str | None = None,
name_contains: str | None = None,
):
"""Build the list query; returned as a SQLAlchemy select for pagination."""
sql = _base_query()
if county:
sql = sql.where(NMW_WellLocations.county == county)
if name_contains:
sql = sql.where(NMW_WellHeaders.cur_well_nam.ilike(f"%{name_contains}%"))
return sql.order_by(NMW_WellHeaders.cur_well_nam)


def geothermal_wells_transformer(rows) -> list[dict]:
"""Map (header, location) Rows -> GeothermalWellResponse dicts."""
return [_to_response(header, location).model_dump() for header, location in rows]


def get_geothermal_well_by_id(session, well_data_id: UUID):
"""Return a single geothermal well by legacy WellDataID, or None."""
row = session.execute(
_base_query().where(NMW_WellHeaders.well_data_id == well_data_id)
).first()
if row is None:
return None
header, location = row
return _to_response(header, location)


# ============= EOF =============================================
Loading
Loading