Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
826a6b6
feat: pass bdd test for well completion date
jacob-a-brown Nov 11, 2025
b34f62e
feat: pass well driller name bdd test
jacob-a-brown Nov 11, 2025
3d89a3e
feat: implement well construction method
jacob-a-brown Nov 11, 2025
ea00c9c
feat: implement well casing diameter in inches
jacob-a-brown Nov 11, 2025
cd4e174
feat: implement well pump type
jacob-a-brown Nov 11, 2025
ca6b6c4
feat: implement well pump depth
jacob-a-brown Nov 11, 2025
a0d939f
Merge branch 'bdms-227' into bdms-227-jab-updates-to-pass-tests
jacob-a-brown Nov 12, 2025
e1df131
fix: remove duplicate lexicon
jacob-a-brown Nov 12, 2025
8e70c85
Merge branch 'bdms-227-jab-bdms-229' into bdms-227-jab-updates-to-pas…
jacob-a-brown Nov 13, 2025
08d7aed
feat: update tests to include well casing materials
jacob-a-brown Nov 13, 2025
3000e79
Merge branch 'kas-bdms-221-225-core-well-info-models-schemas' into ka…
ksmuczynski Nov 13, 2025
7923551
feat: add 'AquiferSystem' model with relationships and controlled voc…
ksmuczynski Nov 14, 2025
4ddec89
refactor: remove completed TODO about lexicon updates
ksmuczynski Nov 14, 2025
29d7b91
refactor: clarify relationship name and add association proxy
ksmuczynski Nov 14, 2025
0d3f807
refactor: clarify proxy purpose doc statement
ksmuczynski Nov 14, 2025
c3393d9
feat: add ThingAquiferAssociation model and update Thing relationships
ksmuczynski Nov 16, 2025
af092dd
feat: add `aqufiers` proxy to Thing model
ksmuczynski Nov 16, 2025
e330df4
feat: Link WellScreen to AquiferSystem
ksmuczynski Nov 16, 2025
04a28be
feat: add GeologicFormation and ThingFormationAssociation models with…
ksmuczynski Nov 17, 2025
477247a
feat(lexicon): add "formation_code" category and associated terms to …
ksmuczynski Nov 17, 2025
d359fa1
rafactor(model): Remove TODO about adding formation_code values to le…
ksmuczynski Nov 17, 2025
fc81327
refactor(lexicon): refine terms associated with "geographic_scale".
ksmuczynski Nov 17, 2025
b01d172
refactor(model): Replace hardcoded srid with SRID_WGS84 from constants
ksmuczynski Nov 18, 2025
f35e61d
refactor(model): Add geologic_formation foreign key to WellScreen model.
ksmuczynski Nov 18, 2025
390d6f3
refactor(model): enhance relationships for `ThingAquiferAssociation` …
ksmuczynski Nov 18, 2025
c738afb
refactor(model): refactor `lithology` field to a lexicon_term
ksmuczynski Nov 18, 2025
4d55177
feat(core): create placeholder `formations.json`
ksmuczynski Nov 18, 2025
8362f9c
refactor: add aquifer and geology related models to `db/__init__.py`
ksmuczynski Nov 19, 2025
39aeefa
feat: add aquifer and geology related enums.
ksmuczynski Nov 19, 2025
47b8415
feat(schemas): add response schemas for aquifer systems and geologic …
ksmuczynski Nov 19, 2025
d1149f5
refactor: rename `ThingFormationAssociation` usages to `ThingGeologic…
ksmuczynski Nov 19, 2025
c85f971
refactor: rename `ThingFormationAssociation` file`ThingGeologicFormat…
ksmuczynski Nov 19, 2025
853e450
refactor(schema): remove aquifer and formation field validators from …
ksmuczynski Nov 19, 2025
e9639bf
feat(model): Add `AquiferType` model and rename `aquifer_type` to `pr…
ksmuczynski Nov 23, 2025
1de7f47
Merge branch 'bdms-227' into kas-227-231-additional-well-info-models
ksmuczynski Nov 24, 2025
ba2b296
feat(model): add eager loading
ksmuczynski Nov 24, 2025
910d5b3
feat(schema): refactor GeoJSON responses for `aquifer_system` and `ge…
ksmuczynski Nov 24, 2025
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
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ repos:
'--statistics'
]
exclude: ^db/__init__.py$ # all models need to be imported for Alembic, but are not used directly
# - repo: local
# hooks:
# - id: pytest
# name: pytest
# entry: pytest # Or your specific test command, e.g., poetry run pytest
# language: system
# types: [python] # Specify relevant file types for your tests
# pass_filenames: false
# always_run: true
# - repo: local
# hooks:
# - id: pytest
# name: pytest
# entry: pytest # Or your specific test command, e.g., poetry run pytest
# language: system
# types: [python] # Specify relevant file types for your tests
# pass_filenames: false
# always_run: true

# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.10.0 # Use the latest stable version or pin to your preference
Expand Down
8 changes: 7 additions & 1 deletion core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
)
CasingMaterial: type[Enum] = build_enum_from_lexicon_category("casing_material")
CollectionMethod: type[Enum] = build_enum_from_lexicon_category("collection_method")
ConstructionMethod: type[Enum] = build_enum_from_lexicon_category("construction_method")
WellConstructionMethod: type[Enum] = build_enum_from_lexicon_category(
"well_construction_method"
)
ContactType: type[Enum] = build_enum_from_lexicon_category("contact_type")
CoordinateMethod: type[Enum] = build_enum_from_lexicon_category("coordinate_method")
WellPurpose: type[Enum] = build_enum_from_lexicon_category("well_purpose")
Expand Down Expand Up @@ -72,4 +74,8 @@
MonitoringFrequency: type[Enum] = build_enum_from_lexicon_category(
"monitoring_frequency"
)
WellPumpType: type[Enum] = build_enum_from_lexicon_category("well_pump_type")
AquiferType: type[Enum] = build_enum_from_lexicon_category("aquifer_type")
GeographicScale: type[Enum] = build_enum_from_lexicon_category("geographic_scale")
Lithology: type[Enum] = build_enum_from_lexicon_category("lithology")
# ============= EOF =============================================
Empty file added core/formations.json
Empty file.
415 changes: 405 additions & 10 deletions core/lexicon.json
Comment thread
ksmuczynski marked this conversation as resolved.

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
from db.transducer import *
from db.measuring_point_history import *
from db.data_provenance import *
from db.aquifer_system import *
from db.geologic_formation import *
from db.thing_aquifer_association import *
from db.thing_geologic_formation_association import *
from db.aquifer_type import *

from sqlalchemy import (
func,
Expand Down
84 changes: 84 additions & 0 deletions db/aquifer_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
SQLAlchemy model for the AquiferSystem table.

This is a master reference table for aquifer systems and hydrogeologic units.
"""

from typing import List, TYPE_CHECKING

from sqlalchemy import Text, Index
from sqlalchemy.orm import relationship, Mapped, mapped_column
from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy
from geoalchemy2 import Geometry

from db.base import Base, AutoBaseMixin, ReleaseMixin
from db.lexicon import lexicon_term

from constants import SRID_WGS84

if TYPE_CHECKING:
from db.thing import WellScreen, ThingAquiferAssociation, Thing
from db.aquifer_type import AquiferType


class AquiferSystem(Base, AutoBaseMixin, ReleaseMixin):
__versioned__ = {}

name: Mapped[str] = mapped_column(
nullable=False,
unique=True,
comment="The full, human-readable name of the aquifer system (e.g., 'Ogallala Aquifer').",
)
description: Mapped[str] = mapped_column(
Text,
nullable=True,
comment="A detailed description of the aquifer system, its characteristics, and its significance.",
)
# Lexicon terms were retrieved from NMAquifer's 'LU_AquiferType' table.
primary_aquifer_type: Mapped[str] = lexicon_term(
nullable=False,
comment="A controlled vocabulary field to classify the aquifer system as a whole (e.g., 'Unconfined', 'Confined', 'Perched').",
)
geographic_scale: Mapped[str] = lexicon_term(
nullable=False,
comment="A controlled vocabulary field to classify the aquifer's geographic scale (e.g., 'Major', 'Regional', 'Local').",
)
boundary: Mapped[Geometry] = mapped_column(
Geometry(geometry_type="MULTIPOLYGON", srid=SRID_WGS84, spatial_index=True),
nullable=True,
comment="A spatial representation of the aquifer system's boundary.",
)
# Hierarchical relationship fields (may be implemented in future iterations)
# Example: High Plains Aquifer (parent) contains Ogallala Aquifer (child)
# parent_id = Column(Integer, ForeignKey('aquifer_system.id'))
# parent = relationship('AquiferSystem', remote_side=[id], backref='subsystems')

# --- Relationships ---
# One-To-Many: An AquiferSystem can be associated with many wells (Things) via the ThingAquiferAssociation join table.
thing_associations: Mapped[List["ThingAquiferAssociation"]] = relationship(
"ThingAquiferAssociation",
back_populates="aquifer_system",
cascade="all, delete-orphan",
passive_deletes=True,
)

# One-To-Many: An AquiferSystem can be the target for many individual WellScreens.
well_screens: Mapped[List["WellScreen"]] = relationship(
"WellScreen",
back_populates="aquifer_system",
cascade="all, delete-orphan",
passive_deletes=True,
)

# --- Association Proxies ---
# Proxy to directly access Things (wells) associated with this AquiferSystem.
things: AssociationProxy[List["Thing"]] = association_proxy(
"thing_associations", "thing"
)
Comment thread
ksmuczynski marked this conversation as resolved.
# Proxy to directly access all AquiferTypes associated with this AquiferSystem.
aquifer_types: AssociationProxy[List["AquiferType"]] = association_proxy(
"thing_associations", "aquifer_types"
)

# --- Table Arguments ---
__table_args__ = Index("ix_aquifersystem_name", "name")
58 changes: 58 additions & 0 deletions db/aquifer_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
SQLAlchemy model for the AquiferType table.

This table stores the specific aquifer characteristics/types associated with
a Thing-AquiferSystem relationship. It allows capturing that a single aquifer
can have multiple characteristics simultaneously.

Example:
A well in the "Ogallala" aquifer might tap portions that are both
"Fractured" AND "Confined". This would create:
- One AquiferSystem: "Ogallala"
- One ThingAquiferAssociation: linking well to Ogallala
- Two AquiferType records: "Fractured" and "Confined"
"""

from typing import TYPE_CHECKING

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column

from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term

if TYPE_CHECKING:
from db.thing_aquifer_association import ThingAquiferAssociation


class AquiferType(Base, AutoBaseMixin, ReleaseMixin):
"""
Represents the specific aquifer types/characteristics for a
Thing-AquiferSystem association.

This allows modeling the fact that:
- A single aquifer can have multiple characteristics
- Different wells may tap different characteristics of the same aquifer
- Characteristics are attributes of the relationship, not the aquifer itself

Fields from WellData CSV:
- AquiferType: May contain multiple codes (e.g., "FC" = Fractured + Confined)
- Each code becomes a separate AquiferType record
"""

# --- Columns ---
thing_aquifer_association_id: Mapped[int] = mapped_column(
ForeignKey("thing_aquifer_association.id", ondelete="CASCADE"),
nullable=False,
comment="Links to the Thing-Aquifer association this type describes.",
)
aquifer_type: Mapped[str] = lexicon_term(
nullable=False,
comment="Controlled vocabulary for aquifer hydrologic properties. "
"Examples: 'Unconfined', 'Confined', 'Perched', 'Fractured', 'Unconsolidated'.",
)

# --- Relationships ---
# Many-to-One: Multiple aquifer types can belong to one association
thing_aquifer_association: Mapped["ThingAquiferAssociation"] = relationship(
"ThingAquiferAssociation", back_populates="aquifer_types"
)
85 changes: 85 additions & 0 deletions db/geologic_formation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
SQLAlchemy model for the GeologicFormation table.

This table is a master reference table for geologic formations. Its purpose is to store definitions and descriptions
of various geologic formations that can be referenced by other tables in the database.
"""

from typing import List, TYPE_CHECKING

from sqlalchemy import Text, Index
from sqlalchemy.orm import relationship, Mapped, mapped_column
from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy
from geoalchemy2 import Geometry

from db.base import Base, AutoBaseMixin, ReleaseMixin
from db.lexicon import lexicon_term

from constants import SRID_WGS84

if TYPE_CHECKING:
from db.thing import Thing, WellScreen
from db.thing_geologic_formation_association import (
ThingGeologicFormationAssociation,
)


class GeologicFormation(Base, AutoBaseMixin, ReleaseMixin):
__versioned__ = {}

# TODO: Let the API map formation codes to names using a formations.json file that can be periodically updated
# from the authoritative source (.e.g USGS). A placeholder `formations.json` file had been added to the `core`
# directory.
# name: Mapped[str] = mapped_column(
# nullable=False,
# unique=True,
# comment="The full, human-readable name of the geologic formation (e.g., 'Navajo Sandstone').",
# )
formation_code: Mapped[str] = lexicon_term(
nullable=True,
unique=True,
comment="A short code or abbreviation for the geologic formation (e.g., '120ELRT').",
)
description: Mapped[str] = mapped_column(
Text,
nullable=True,
comment="A detailed description of the geologic formation, its characteristics, and its significance.",
)
# TODO: Implement controlled vocabularies for `lithology` using NMAquifer's 'LU_Lithology' table.
# This should be implemented after AMMP reviews and cleans up their formation terms and codes.
lithology: Mapped[str] = lexicon_term(
nullable=True,
comment="A controlled vocabulary for the primary, dominant rock type"
"(e.g., 'Tuff', 'Sandstone', 'Alluvium', 'Shale').",
)
boundary: Mapped[Geometry] = mapped_column(
Geometry(geometry_type="MULTIPOLYGON", srid=SRID_WGS84, spatial_index=True),
nullable=True,
comment="A spatial representation of the geologic formation's extent.",
)

# --- Relationships ---
# One-To-Many (Association Object): A GeologicFormation can be associated with many Things (e.g., wells) via the
# ThingGeologicFormationAssociation join table.
thing_associations: Mapped[List["ThingGeologicFormationAssociation"]] = (
relationship(
"ThingGeologicFormationAssociation",
back_populates="geologic_formation",
cascade="all, delete-orphan",
passive_deletes=True,
)
)
# One-To-Many: A GeologicFormation can have many physical WellScreens installed in it.
screens: Mapped[List["WellScreen"]] = relationship(
"WellScreen", back_populates="geologic_formation", passive_deletes=True
)

# --- Association Proxies ---
# Provides direct access to Things (wells) that penetrate this formation.
things: AssociationProxy["Thing"] = association_proxy("thing_associations", "thing")

# --- Table Arguments ---
__table_args__ = (
Index("ix_geologicformation_name", "name"),
Index("ix_geologicformation_code", "code"),
)
61 changes: 61 additions & 0 deletions db/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
from db.sensor import Sensor
from db.contact import Contact
from db.group import Group, GroupThingAssociation
from db.aquifer_system import AquiferSystem
from db.thing_aquifer_association import ThingAquiferAssociation
from db.geologic_formation import GeologicFormation
from db.thing_geologic_formation_association import (
ThingGeologicFormationAssociation,
)


class Thing(
Expand Down Expand Up @@ -116,6 +122,21 @@ class Thing(

well_construction_notes: Mapped[str] = mapped_column(Text, nullable=True)

well_completion_date: Mapped[str] = mapped_column(
Date, nullable=True, comment="the date the well was completed if known"
)
well_driller_name: Mapped[str] = mapped_column(
String(200), nullable=True, comment="Name of the well driller."
)
well_construction_method = lexicon_term(nullable=True)
well_pump_type: Mapped[str] = lexicon_term(nullable=True)
well_pump_depth: Mapped[float] = mapped_column(
Float,
nullable=True,
info={"unit": "feet below ground surface"},
comment="Depth of the well pump from ground surface to the pump intake (in feet).",
)

# Spring-related columns
spring_type: Mapped[str] = lexicon_term(
nullable=True,
Expand Down Expand Up @@ -263,6 +284,26 @@ class Thing(
lazy="joined",
)

# One-To-Many: A Thing can be associated with many AquiferSystems via the ThingAquiferAssociation join table.
aquifer_associations: Mapped[List["ThingAquiferAssociation"]] = relationship(
"ThingAquiferAssociation",
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

# Many-To-Many: A Thing can penetrate many GeologicFormations.
formation_associations: Mapped[List["ThingGeologicFormationAssociation"]] = (
relationship(
"ThingGeologicFormationAssociation",
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)
)

# --- Association Proxies ---
assets: AssociationProxy[list["Asset"]] = association_proxy(
"asset_associations", "asset"
Expand All @@ -288,6 +329,16 @@ class Thing(
"group_associations", "group"
)

# Proxy to directly access AquiferSystems associated with this Thing
aquifers: AssociationProxy[List["AquiferSystem"]] = association_proxy(
"aquifer_associations", "aquifer_system"
)

# Proxy to directly access the GeologicFormations penetrated by this Thing.
formations: AssociationProxy[List["GeologicFormation"]] = association_proxy(
"formation_associations", "geologic_formation"
)

# Full-text search vector
search_vector = Column(TSVectorType("name", "well_construction_notes"))

Expand Down Expand Up @@ -406,6 +457,12 @@ class WellScreen(Base, AutoBaseMixin, ReleaseMixin):
thing_id: Mapped[int] = mapped_column(
ForeignKey("thing.id", ondelete="CASCADE"), nullable=False
)
aquifer_system_id: Mapped[int] = mapped_column(
ForeignKey("aquifer_system.id", ondelete="SET NULL"), nullable=True
)
geologic_formation_id: Mapped[int] = mapped_column(
ForeignKey("geologic_formation.id", ondelete="SET NULL"), nullable=True
)
screen_depth_top: Mapped[float] = mapped_column(
info={"unit": "feet below ground surface"}, nullable=True
)
Expand All @@ -423,6 +480,10 @@ class WellScreen(Base, AutoBaseMixin, ReleaseMixin):
# Many-To-One: A WellScreen belongs to one Thing.
thing: Mapped["Thing"] = relationship("Thing", back_populates="screens")

aquifer_system: Mapped["AquiferSystem"] = relationship(
"AquiferSystem", back_populates="well_screens", passive_deletes=True
)


class WellPurpose(Base, AutoBaseMixin, ReleaseMixin):
"""
Expand Down
Loading
Loading