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 * diff --git a/db/deployment.py b/db/deployment.py new file mode 100644 index 000000000..78eaa4ef0 --- /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 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, 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.id"), nullable=False + ) + sensor_id: Mapped[int] = mapped_column( + Integer, ForeignKey("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(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) + 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") diff --git a/db/sensor.py b/db/sensor.py index 6ab04b7c9..bc3f243fb 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 db.deployment import Deployment + from db.thing import Thing + from db.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 ============================================= diff --git a/db/thing.py b/db/thing.py index f09fa4930..7da938744 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. + sensors: AssociationProxy[List["Sensor"]] = association_proxy( + "deployments", "sensor" + ) + class ThingIdLink(Base, AutoBaseMixin, ReleaseMixin): """