From 3f97044c8749db15991ed3d9e79fe26d9270be14 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 10:04:37 -0600 Subject: [PATCH 1/8] feat: create new Deployment model --- db/deployment.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 db/deployment.py diff --git a/db/deployment.py b/db/deployment.py new file mode 100644 index 000000000..feee78832 --- /dev/null +++ b/db/deployment.py @@ -0,0 +1,47 @@ +""" +This table is an installation log that creates a many-to-many relationship +between Things and Equipment, tracking which piece of hardware was installed +at which Thing and for what period of time. +""" + +from typing import TYPE_CHECKING + +from sqlalchemy import Integer, ForeignKey, String, Date, Numeric, Text +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term + +if TYPE_CHECKING: + from db.thing import Thing + from db.sensor import Sensor + + +class Deployment(Base, AutoBaseMixin, ReleaseMixin): + """ + Represents the installation of a specific piece of equipment (Sensor) at a Thing + for a defined period. This is an Association Object. + """ + + # --- Foreign Keys --- + thing_id: Mapped[int] = mapped_column( + Integer, ForeignKey("thing.thing_id"), nullable=False + ) + sensor_id: Mapped[int] = mapped_column( + Integer, ForeignKey("sensor.sensor_id"), nullable=False + ) + + # --- Columns --- + installation_date: Mapped[Date] = mapped_column(Date, nullable=False) + removal_date: Mapped[Date] = mapped_column(Date, nullable=True) + recording_interval: Mapped[int] = mapped_column(String(50), nullable=True) + recording_interval_units: Mapped[str] = lexicon_term(nullable=True) + hanging_cable_length: Mapped[float] = mapped_column(Numeric, nullable=True) + hanging_point_height: Mapped[float] = mapped_column(Numeric, nullable=True) + hanging_point_description: Mapped[str] = mapped_column(Text, nullable=True) + notes: Mapped[str] = mapped_column(Text, nullable=True) + + # --- Relationships --- + # Many-To-One: A Deployment is for one Thing. + thing: Mapped["Thing"] = relationship("Thing", back_populates="deployments") + # Many-To-One: A Deployment is of one piece of equipment (sensor). + sensor: Mapped["Sensor"] = relationship("Sensor", back_populates="deployments") From 1a01fd9e9fe8eda55abd84aca4d2375673f92a15 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 10:53:35 -0600 Subject: [PATCH 2/8] refactor: modify `mapped_column` for `recording_interval` field --- db/deployment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/deployment.py b/db/deployment.py index feee78832..94068ac57 100644 --- a/db/deployment.py +++ b/db/deployment.py @@ -1,12 +1,12 @@ """ This table is an installation log that creates a many-to-many relationship -between Things and Equipment, tracking which piece of hardware was installed +between Things and Sensors, tracking which piece of hardware was installed at which Thing and for what period of time. """ from typing import TYPE_CHECKING -from sqlalchemy import Integer, ForeignKey, String, Date, Numeric, Text +from sqlalchemy import Integer, ForeignKey, Date, Numeric, Text from sqlalchemy.orm import relationship, Mapped, mapped_column from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term @@ -33,7 +33,7 @@ class Deployment(Base, AutoBaseMixin, ReleaseMixin): # --- Columns --- installation_date: Mapped[Date] = mapped_column(Date, nullable=False) removal_date: Mapped[Date] = mapped_column(Date, nullable=True) - recording_interval: Mapped[int] = mapped_column(String(50), nullable=True) + recording_interval: Mapped[int] = mapped_column(Integer, nullable=True) recording_interval_units: Mapped[str] = lexicon_term(nullable=True) hanging_cable_length: Mapped[float] = mapped_column(Numeric, nullable=True) hanging_point_height: Mapped[float] = mapped_column(Numeric, nullable=True) From c22fd2bd8414f8d74ac6e07fb54cb42b186bc8d7 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 12:14:01 -0600 Subject: [PATCH 3/8] feat: add `deployment` relationship and proxy to `Thing` model refactor: update `field_event` proxy to SQLAlchemy 2.0 syntax --- db/thing.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/db/thing.py b/db/thing.py index f09fa4930..7441bbf62 100644 --- a/db/thing.py +++ b/db/thing.py @@ -22,6 +22,15 @@ from db.asset import Asset from db.base import AutoBaseMixin, Base, ReleaseMixin +from typing import List, TYPE_CHECKING + +if TYPE_CHECKING: + from db.location import Location + from db.field import FieldEvent + from db.deployment import Deployment + from db.sensor import Sensor + from db.contact import Contact + class Thing(Base, AutoBaseMixin, ReleaseMixin): @@ -87,10 +96,23 @@ class Thing(Base, AutoBaseMixin, ReleaseMixin): ) ) - field_events = relationship( + # --- Relationships --- + # One-To-Many: A Thing can have many FieldEvents over time. + field_events: Mapped[List["FieldEvent"]] = relationship( "FieldEvent", back_populates="thing", cascade="all, delete-orphan", uselist=True ) + # One-To-Many: A Thing can have many Deployments of sensors (equipment) over time. + deployments: Mapped[List["Deployment"]] = relationship( + "Deployment", back_populates="thing", cascade="all, delete-orphan" + ) + + # --- Association Proxies --- + # Proxy to directly access the Sensor deployed at this Thing. + sensor: AssociationProxy[List["Sensor"]] = association_proxy( + "deployments", "sensor" + ) + class ThingIdLink(Base, AutoBaseMixin, ReleaseMixin): """ From 441a5eae888cb4a8858fe70d79a6d3286405bde7 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 12:31:29 -0600 Subject: [PATCH 4/8] feat: add `deployment` relationship and `thing` proxy to the `Sensor` table --- db/sensor.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/db/sensor.py b/db/sensor.py index 6ab04b7c9..fe5de28b2 100644 --- a/db/sensor.py +++ b/db/sensor.py @@ -16,10 +16,18 @@ from datetime import datetime from sqlalchemy import String, Integer, DateTime +from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy from sqlalchemy.orm import relationship, mapped_column, Mapped from db.base import Base, AutoBaseMixin, ReleaseMixin +from typing import List, TYPE_CHECKING + +if TYPE_CHECKING: + from .deployment import Deployment + from .thing import Thing + from .observation import Observation + class Sensor(Base, AutoBaseMixin, ReleaseMixin): """ @@ -40,10 +48,21 @@ class Sensor(Base, AutoBaseMixin, ReleaseMixin): recording_interval: Mapped[int] = mapped_column(Integer, nullable=True) notes: Mapped[str] = mapped_column(String(50), nullable=True) - observations: Mapped[list["Observation"]] = relationship( # noqa: F821 + # --- Relationships --- + # One-To-Many: A piece of Equipment can generate many Observations. + observations: Mapped[List["Observation"]] = relationship( # noqa: F821 "Observation", back_populates="sensor", ) + # One-To-Many: A Sensor (or piece of equipment) can have many Deployments over its lifetime. + deployments: Mapped[List["Deployment"]] = relationship( + "Deployment", back_populates="sensor" + ) + + # --- Association Proxies --- + # Proxy to directly access the Things where this Equipment has been deployed. + things: AssociationProxy[List["Thing"]] = association_proxy("deployments", "thing") + # ============= EOF ============================================= From 9b2ed77e6dba8e7b4137d9513806db574e7f1846 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 12:39:12 -0600 Subject: [PATCH 5/8] refactor: import necessary classes to `Sensor` model so they can be found by relationships. --- db/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/sensor.py b/db/sensor.py index fe5de28b2..bc3f243fb 100644 --- a/db/sensor.py +++ b/db/sensor.py @@ -24,9 +24,9 @@ from typing import List, TYPE_CHECKING if TYPE_CHECKING: - from .deployment import Deployment - from .thing import Thing - from .observation import Observation + from db.deployment import Deployment + from db.thing import Thing + from db.observation import Observation class Sensor(Base, AutoBaseMixin, ReleaseMixin): From 5ae809403fbb1244e228fd1f1b241718d799b43d Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 13:06:45 -0600 Subject: [PATCH 6/8] refactor: update foreign key names in `Deployment` table --- db/deployment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/deployment.py b/db/deployment.py index 94068ac57..78eaa4ef0 100644 --- a/db/deployment.py +++ b/db/deployment.py @@ -24,10 +24,10 @@ class Deployment(Base, AutoBaseMixin, ReleaseMixin): # --- Foreign Keys --- thing_id: Mapped[int] = mapped_column( - Integer, ForeignKey("thing.thing_id"), nullable=False + Integer, ForeignKey("thing.id"), nullable=False ) sensor_id: Mapped[int] = mapped_column( - Integer, ForeignKey("sensor.sensor_id"), nullable=False + Integer, ForeignKey("sensor.id"), nullable=False ) # --- Columns --- From 7ff9e159d32f40506764406c1bd2b4d8b59c696e Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 13:15:24 -0600 Subject: [PATCH 7/8] feat: add `deployment` model to `__init__` file so mappers can be configured.. --- db/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/db/__init__.py b/db/__init__.py index b75475493..e448b2b9e 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -22,6 +22,7 @@ from db.asset import * from db.collabnet import * from db.contact import * +from db.deployment import * from db.geochronology import * from db.geothermal import * from db.field import * From f4ffb6c2d98745b040e4218494c276ecd55c0547 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 24 Sep 2025 13:25:12 -0600 Subject: [PATCH 8/8] refactor: update proxy name in `Thing` table from `sensor` to `sensors`. --- db/thing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/thing.py b/db/thing.py index 7441bbf62..7da938744 100644 --- a/db/thing.py +++ b/db/thing.py @@ -109,7 +109,7 @@ class Thing(Base, AutoBaseMixin, ReleaseMixin): # --- Association Proxies --- # Proxy to directly access the Sensor deployed at this Thing. - sensor: AssociationProxy[List["Sensor"]] = association_proxy( + sensors: AssociationProxy[List["Sensor"]] = association_proxy( "deployments", "sensor" )