From dab661a0349fbc7196acc48c006311d73bb5a789 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 12:32:13 -0600 Subject: [PATCH 1/8] feat: create `AnalysisMethod` table and add to `__init__.py` file --- db/__init__.py | 1 + db/analysis_method.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 db/analysis_method.py diff --git a/db/__init__.py b/db/__init__.py index e448b2b9e..bf0db93be 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -19,6 +19,7 @@ from db.base import * from db.base import Base +from db.analysis_method import * from db.asset import * from db.collabnet import * from db.contact import * diff --git a/db/analysis_method.py b/db/analysis_method.py new file mode 100644 index 000000000..6eefe70d8 --- /dev/null +++ b/db/analysis_method.py @@ -0,0 +1,45 @@ +""" +This table is a citable library of abstract analytical methods and +standardized field procedures. +""" + +from typing import List, TYPE_CHECKING + +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from db.base import Base + +if TYPE_CHECKING: + from db.observation import Observation + + +class AnalysisMethod(Base): + """ + Represents a single, citable analytical method or standard procedure. + This is a lookup table. + """ + + # --- Columns --- + method_code: Mapped[str] = mapped_column( + nullable=True, + unique=True, + comment="The official code or identifier for the method (e.g., 'EPA 300.0').", + ) + method_name: Mapped[str] = mapped_column( + nullable=False, + comment="The common, human-readable name of the method (e.g., Ion Chromatography for Anions).", + ) + method_type: Mapped[str] = mapped_column( + nullable=True, + comment="A controlled vocabulary field to categorize the method (e.g., 'Laboratory', 'Field Procedure', 'Calculation').", + ) + source_organization: Mapped[str] = mapped_column( + nullable=True, + comment="The organization that published the method (e.g., 'US EPA', 'USGS').", + ) + + # --- Relationships --- + # One-To-Many: An AnalysisMethod can be used for many Observations. + observations: Mapped[List["Observation"]] = relationship( + "Observation", back_populates="method" + ) From 6496a8e577470cfb0c18583f13da847c8e47346a Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 13:22:40 -0600 Subject: [PATCH 2/8] feat: Add `AnalysisMethod` relationship and foreign key to `Observation` table --- db/analysis_method.py | 2 +- db/observation.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/db/analysis_method.py b/db/analysis_method.py index 6eefe70d8..ffa656cea 100644 --- a/db/analysis_method.py +++ b/db/analysis_method.py @@ -41,5 +41,5 @@ class AnalysisMethod(Base): # --- Relationships --- # One-To-Many: An AnalysisMethod can be used for many Observations. observations: Mapped[List["Observation"]] = relationship( - "Observation", back_populates="method" + "Observation", back_populates="analysis_method" ) diff --git a/db/observation.py b/db/observation.py index 720fcda0f..af737a7cf 100644 --- a/db/observation.py +++ b/db/observation.py @@ -17,11 +17,19 @@ from sqlalchemy import ( ForeignKey, DateTime, + Integer, ) from sqlalchemy.orm import mapped_column, relationship, Mapped from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from db.sample import Sample + from db.sensor import Sensor + from db.analysis_method import AnalysisMethod + class Observation(Base, AutoBaseMixin, ReleaseMixin): __versioned__ = {} @@ -29,6 +37,7 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): # NM_Aquifer fields for audits nma_pk_waterlevels: Mapped[str] = mapped_column(nullable=True) + # --- Foreign Keys --- sample_id: Mapped[int] = mapped_column( ForeignKey("sample.id", ondelete="CASCADE"), nullable=False, @@ -37,7 +46,11 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): ForeignKey("sensor.id", ondelete="CASCADE"), nullable=True, ) + analysis_method_id: Mapped[int] = mapped_column( + Integer, ForeignKey("analysis_method.id"), nullable=True + ) + # --- Columns --- observation_datetime: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, doc="Timestamp of the observation" ) @@ -63,12 +76,21 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): doc="Depth of the geothermal observation in feet", ) + # --- Relationships --- + # Many-To-One: An Observation can be generated by one piece of Equipment. sensor: Mapped["Sensor"] = relationship( # noqa: F821 "Sensor", back_populates="observations", passive_deletes=True ) # noqa: F821 + + # Many-To-One: An Observation is derived from one Sample. sample: Mapped["Sample"] = relationship( # noqa: F821 "Sample", back_populates="observations", passive_deletes=True ) # noqa: F821 + # Many-To-One: An Observation can be generated using one AnalysisMethod. + analysis_method: Mapped["AnalysisMethod"] = relationship( + "AnalysisMethod", back_populates="observations" + ) + # ============= EOF ============================================= From ee87395d9b75fb9ac32894d65f094f6381b4bb48 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 13:24:53 -0600 Subject: [PATCH 3/8] refactor: rename columns to make clear fields are related to analysis methods, not sample methods. --- db/analysis_method.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/analysis_method.py b/db/analysis_method.py index ffa656cea..f46029bd2 100644 --- a/db/analysis_method.py +++ b/db/analysis_method.py @@ -20,16 +20,16 @@ class AnalysisMethod(Base): """ # --- Columns --- - method_code: Mapped[str] = mapped_column( + analysis_method_code: Mapped[str] = mapped_column( nullable=True, unique=True, comment="The official code or identifier for the method (e.g., 'EPA 300.0').", ) - method_name: Mapped[str] = mapped_column( + analysis_method_name: Mapped[str] = mapped_column( nullable=False, comment="The common, human-readable name of the method (e.g., Ion Chromatography for Anions).", ) - method_type: Mapped[str] = mapped_column( + analysis_method_type: Mapped[str] = mapped_column( nullable=True, comment="A controlled vocabulary field to categorize the method (e.g., 'Laboratory', 'Field Procedure', 'Calculation').", ) From 6ad7ad70ab281908db56f381b158896e8e8587db Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 13:45:44 -0600 Subject: [PATCH 4/8] feat: add valid `analysis_method_type` values to the `lexicon.json` file --- core/lexicon.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/lexicon.json b/core/lexicon.json index 14d2a1828..20b8ec4f6 100644 --- a/core/lexicon.json +++ b/core/lexicon.json @@ -341,6 +341,9 @@ {"categories": [{"name": "sample_method", "description": null}], "term": "pump", "definition": "pump"}, {"categories": [{"name": "sample_method", "description": null}], "term": "thief sampler", "definition": "thief sampler"}, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Laboratory", "definition": "A procedure performed on a physical sample in a controlled, off-site laboratory environment. These methods typically involve complex instrumentation, standardized reagents, and formal quality control protocols."}, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Field Procedure", "definition": "A standardized procedure performed on-site at the time of sample collection. This can involve direct measurement of the environmental medium using a calibrated field instrument or a specific, documented technique for collecting a sample."}, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Calculation", "definition": "A mathematical procedure used to derive a new data point from one or more directly measured values. This type is used to document the provenance of calculated data, providing an auditable trail."},, {"categories": [{"name": "organization", "description": null}], "term": "USGS", "definition": "US Geological Survey"}, {"categories": [{"name": "organization", "description": null}], "term": "TWDB", "definition": "Texas Water Development Board"}, From 5a77ac59ceb153c3bdc520a45d10039037713a49 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 13:49:31 -0600 Subject: [PATCH 5/8] refactor: set `analysis_method_type` and `source_organization` fields as lexicon terms --- db/analysis_method.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/db/analysis_method.py b/db/analysis_method.py index f46029bd2..daa1be0ca 100644 --- a/db/analysis_method.py +++ b/db/analysis_method.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import relationship, Mapped, mapped_column from db.base import Base +from tests.conftest import lexicon_term if TYPE_CHECKING: from db.observation import Observation @@ -29,11 +30,11 @@ class AnalysisMethod(Base): nullable=False, comment="The common, human-readable name of the method (e.g., Ion Chromatography for Anions).", ) - analysis_method_type: Mapped[str] = mapped_column( + analysis_method_type: Mapped[str] = lexicon_term( nullable=True, comment="A controlled vocabulary field to categorize the method (e.g., 'Laboratory', 'Field Procedure', 'Calculation').", ) - source_organization: Mapped[str] = mapped_column( + source_organization: Mapped[str] = lexicon_term( nullable=True, comment="The organization that published the method (e.g., 'US EPA', 'USGS').", ) From 27fcfc443f2f4f08e641877b9f2aa11814a9ff84 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 14:10:24 -0600 Subject: [PATCH 6/8] refactor: import `lexicon_term` from db.base --- db/analysis_method.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/db/analysis_method.py b/db/analysis_method.py index daa1be0ca..05798e035 100644 --- a/db/analysis_method.py +++ b/db/analysis_method.py @@ -7,8 +7,7 @@ from sqlalchemy.orm import relationship, Mapped, mapped_column -from db.base import Base -from tests.conftest import lexicon_term +from db.base import Base, lexicon_term if TYPE_CHECKING: from db.observation import Observation From ed4d5a4f67382fb0c3885492ee4fe84f4e87e995 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 14:12:34 -0600 Subject: [PATCH 7/8] refactor: import mixins --- db/analysis_method.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/analysis_method.py b/db/analysis_method.py index 05798e035..631668400 100644 --- a/db/analysis_method.py +++ b/db/analysis_method.py @@ -7,13 +7,13 @@ from sqlalchemy.orm import relationship, Mapped, mapped_column -from db.base import Base, lexicon_term +from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term if TYPE_CHECKING: from db.observation import Observation -class AnalysisMethod(Base): +class AnalysisMethod(Base, AutoBaseMixin, ReleaseMixin): """ Represents a single, citable analytical method or standard procedure. This is a lookup table. From 6669ea9823b34111c86f95f0e8b9029e33a8b2c3 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 25 Sep 2025 14:28:25 -0600 Subject: [PATCH 8/8] refactor: fix typo in `lexicon.py` file --- core/lexicon.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lexicon.json b/core/lexicon.json index 20b8ec4f6..dbb268673 100644 --- a/core/lexicon.json +++ b/core/lexicon.json @@ -343,7 +343,7 @@ {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Laboratory", "definition": "A procedure performed on a physical sample in a controlled, off-site laboratory environment. These methods typically involve complex instrumentation, standardized reagents, and formal quality control protocols."}, {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Field Procedure", "definition": "A standardized procedure performed on-site at the time of sample collection. This can involve direct measurement of the environmental medium using a calibrated field instrument or a specific, documented technique for collecting a sample."}, - {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Calculation", "definition": "A mathematical procedure used to derive a new data point from one or more directly measured values. This type is used to document the provenance of calculated data, providing an auditable trail."},, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Calculation", "definition": "A mathematical procedure used to derive a new data point from one or more directly measured values. This type is used to document the provenance of calculated data, providing an auditable trail."}, {"categories": [{"name": "organization", "description": null}], "term": "USGS", "definition": "US Geological Survey"}, {"categories": [{"name": "organization", "description": null}], "term": "TWDB", "definition": "Texas Water Development Board"},