From ba183e353621eb08e70099f808d3324dc4aa51f4 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 10:41:23 -0600 Subject: [PATCH 1/7] refactor: remove thing_id from sample response --- schemas/sample.py | 5 ++--- tests/test_sample.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/schemas/sample.py b/schemas/sample.py index 0a86b6620..7451ad522 100644 --- a/schemas/sample.py +++ b/schemas/sample.py @@ -24,6 +24,7 @@ from typing import Annotated from typing_extensions import Self +from schemas import ORMBaseModel from schemas.thing import ThingResponse """ @@ -142,9 +143,7 @@ class UpdateSample(ValidateSample): # -------- RESPONSE ---------- -class SampleResponse(BaseModel): - id: int - thing_id: int +class SampleResponse(ORMBaseModel): thing: ThingResponse sample_type: str field_sample_id: str diff --git a/tests/test_sample.py b/tests/test_sample.py index 35961c99b..4003d8f2e 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -78,7 +78,7 @@ def test_add_sample(spring_thing, sensor): ) data = response.json() assert response.status_code == 201 - assert data["thing_id"] == payload["thing_id"] + assert data["thing"]["id"] == spring_thing.id assert data["sample_type"] == payload["sample_type"] assert data["field_sample_id"] == payload["field_sample_id"] assert data["sample_date"] == payload["sample_date"] @@ -248,7 +248,7 @@ def test_409_patch_sample_invalid_thing_id(sample): # ============= Get tests for samples ============================================= -def test_get_samples(sample): +def test_get_samples(sample, water_well_thing): """ Test retrieving samples """ @@ -257,7 +257,7 @@ def test_get_samples(sample): data = response.json() assert len(data["items"]) == 1 assert data["items"][0]["id"] == sample.id - assert data["items"][0]["thing_id"] == sample.thing_id + assert data["items"][0]["thing"]["id"] == water_well_thing.id assert data["items"][0]["sample_type"] == sample.sample_type assert data["items"][0]["field_sample_id"] == sample.field_sample_id assert data["items"][0]["sample_date"] == sample.sample_date @@ -272,7 +272,7 @@ def test_get_samples(sample): assert data["items"][0]["sample_bottom"] == sample.sample_bottom -def test_get_sample_by_id(sample): +def test_get_sample_by_id(sample, water_well_thing): """ Test retrieving a sample by its ID. """ @@ -280,7 +280,7 @@ def test_get_sample_by_id(sample): assert response.status_code == 200 data = response.json() assert data["id"] == sample.id - assert data["thing_id"] == sample.thing_id + assert data["thing"]["id"] == water_well_thing.id assert data["sample_type"] == sample.sample_type assert data["field_sample_id"] == sample.field_sample_id assert data["sample_date"] == sample.sample_date From 06c0e7cdeafded122708af81dd9facda645e2d78 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 10:58:37 -0600 Subject: [PATCH 2/7] feat: create base Create, Update, Response schemas --- schemas/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/schemas/__init__.py b/schemas/__init__.py index 668fd8005..21b9355ba 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from datetime import datetime - from pydantic import BaseModel, ConfigDict, AwareDatetime @@ -22,9 +20,19 @@ class ResourceNotFoundResponse(BaseModel): detail: str -class ORMBaseModel(BaseModel): +class BaseCreateSchema(BaseModel): + release_status: str + + +class BaseUpdateSchema(BaseCreateSchema): + release_status: str | None = None + + +class BaseResponseSchema(BaseModel): id: int # every ORM model should have an id field created_at: AwareDatetime + release_status: str + model_config = ConfigDict( from_attributes=True, populate_by_name=True, From fb103c0384535a6bf007f903827bc23b3cc3d01d Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 12:57:16 -0600 Subject: [PATCH 3/7] refactor: make add_contacts flexible for schema/model updates --- services/contact_helper.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/services/contact_helper.py b/services/contact_helper.py index 8860da7a2..3f7031999 100644 --- a/services/contact_helper.py +++ b/services/contact_helper.py @@ -21,15 +21,19 @@ from sqlalchemy.orm import Session -def add_contact( - session: Session, contact_data: CreateContact | dict, user: dict -) -> Contact: +def add_contact(session: Session, data: CreateContact | dict, user: dict) -> Contact: """ Add a new contact to the database. """ - if isinstance(contact_data, CreateContact): - contact_data = contact_data.model_dump(exclude_unset=True) + if isinstance(data, CreateContact): + data = data.model_dump(exclude_unset=True) + + email_data = data.pop("emails", []) + phone_data = data.pop("phones", []) + address_data = data.pop("addresses", []) + thing_id = data.pop("thing_id", None) + contact_data = data """ Developer's note @@ -39,21 +43,18 @@ def add_contact( """ try: - contact = Contact( - name=contact_data["name"], - role=contact_data["role"], - ) - for e in contact_data.get("emails", []): + contact = Contact(**contact_data) + for e in email_data: email = Email(**e) contact.emails.append(email) # session.add(email) - for p in contact_data.get("phones", []): + for p in phone_data: phone = Phone(**p) contact.phones.append(phone) # session.add(phone) - for a in contact_data.get("addresses", []): + for a in address_data: address = Address(**a) contact.addresses.append(address) # session.add(address) @@ -68,7 +69,7 @@ def add_contact( session.refresh(contact) location_contact_association = ThingContactAssociation() - location_contact_association.thing_id = contact_data.get("thing_id") + location_contact_association.thing_id = thing_id location_contact_association.contact_id = contact.id audit_add(user, location_contact_association) From ed7dea8adceb58f0d41a30df26c03e6f516808af Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 13:19:35 -0600 Subject: [PATCH 4/7] feat: require release_status for all create, optional for all update And test to ensure created_at in all responses --- api/thing.py | 2 +- db/asset.py | 4 +- db/contact.py | 10 +-- db/group.py | 6 +- db/sensor.py | 6 +- db/thing.py | 8 +-- schemas/__init__.py | 6 +- schemas/asset.py | 8 +-- schemas/contact.py | 62 ++++++++-------- schemas/group.py | 8 +-- schemas/lexicon.py | 20 ++++-- schemas/location.py | 20 ++---- schemas/observation.py | 8 +-- schemas/sample.py | 22 +++--- schemas/sensor.py | 10 +-- schemas/thing.py | 27 +++---- tests/conftest.py | 26 ++++++- tests/test_asset.py | 28 +++++++- tests/test_contact.py | 147 ++++++++++++++++++++++++++++++++++---- tests/test_group.py | 9 ++- tests/test_location.py | 5 ++ tests/test_observation.py | 32 ++++++++- tests/test_sample.py | 8 +++ tests/test_sensor.py | 16 ++++- tests/test_thing.py | 29 ++++++++ 25 files changed, 380 insertions(+), 147 deletions(-) diff --git a/api/thing.py b/api/thing.py index bc6578c8c..c38813750 100644 --- a/api/thing.py +++ b/api/thing.py @@ -341,7 +341,7 @@ def create_thing_id_link( link_data: CreateThingIdLink, session: session_dependency, user: admin_dependency, -): +) -> ThingIdLinkResponse: """ Create a new link between a thing and an alternate ID. """ diff --git a/db/asset.py b/db/asset.py index 080490c27..e795734ae 100644 --- a/db/asset.py +++ b/db/asset.py @@ -20,10 +20,10 @@ from sqlalchemy.testing.schema import mapped_column from sqlalchemy_utils import TSVectorType -from db.base import Base, AutoBaseMixin +from db.base import Base, AutoBaseMixin, ReleaseMixin -class Asset(Base, AutoBaseMixin): +class Asset(Base, AutoBaseMixin, ReleaseMixin): # name = Column(String(100), nullable=False, unique=True) # file_type = Column(String(50), nullable=False) # diff --git a/db/contact.py b/db/contact.py index 8d910a879..217582d44 100644 --- a/db/contact.py +++ b/db/contact.py @@ -18,7 +18,7 @@ from sqlalchemy.orm import relationship from sqlalchemy_utils import TSVectorType -from db.base import Base, AutoBaseMixin, lexicon_term +from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term class ThingContactAssociation(Base, AutoBaseMixin): @@ -33,7 +33,7 @@ class ThingContactAssociation(Base, AutoBaseMixin): thing = relationship("Thing") -class Contact(Base, AutoBaseMixin): +class Contact(Base, AutoBaseMixin, ReleaseMixin): name = Column(String(100), nullable=False) role = lexicon_term(nullable=False) @@ -58,7 +58,7 @@ class Contact(Base, AutoBaseMixin): things = association_proxy("thing_associations", "thing") -class Phone(Base, AutoBaseMixin): +class Phone(Base, AutoBaseMixin, ReleaseMixin): contact_id = Column( Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False ) @@ -69,7 +69,7 @@ class Phone(Base, AutoBaseMixin): search_vector = Column(TSVectorType("phone_number")) -class Email(Base, AutoBaseMixin): +class Email(Base, AutoBaseMixin, ReleaseMixin): contact_id = Column( Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False ) @@ -81,7 +81,7 @@ class Email(Base, AutoBaseMixin): search_vector = Column(TSVectorType("email")) -class Address(Base, AutoBaseMixin): +class Address(Base, AutoBaseMixin, ReleaseMixin): contact_id = Column( Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False ) diff --git a/db/group.py b/db/group.py index a406f1b2c..6dbddb89a 100644 --- a/db/group.py +++ b/db/group.py @@ -16,14 +16,14 @@ from typing import Optional from geoalchemy2 import Geometry, WKBElement -from sqlalchemy import Column, String, Integer, ForeignKey +from sqlalchemy import String, Integer, ForeignKey from sqlalchemy.orm import relationship, Mapped from sqlalchemy.testing.schema import mapped_column -from db.base import Base, AutoBaseMixin +from db.base import Base, AutoBaseMixin, ReleaseMixin -class Group(Base, AutoBaseMixin): +class Group(Base, AutoBaseMixin, ReleaseMixin): # --- Column Definitions --- description: Mapped[str] = mapped_column(String(255), nullable=True) name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) diff --git a/db/sensor.py b/db/sensor.py index 01252ef8e..1b7b52166 100644 --- a/db/sensor.py +++ b/db/sensor.py @@ -15,13 +15,13 @@ # =============================================================================== from datetime import datetime -from sqlalchemy import Column, String, Integer, DateTime +from sqlalchemy import String, Integer, DateTime from sqlalchemy.orm import relationship, mapped_column, Mapped -from db.base import Base, AutoBaseMixin +from db.base import Base, AutoBaseMixin, ReleaseMixin -class Sensor(Base, AutoBaseMixin): +class Sensor(Base, AutoBaseMixin, ReleaseMixin): """ Base class for all sensor types. This class can be extended to create specific sensor types. diff --git a/db/thing.py b/db/thing.py index 1fe16e317..0f31e0b45 100644 --- a/db/thing.py +++ b/db/thing.py @@ -13,11 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from xmlrpc.client import Boolean - from sqlalchemy import Integer, ForeignKey, String, Column, Float from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.orm import relationship, mapped_column, Mapped +from sqlalchemy.orm import relationship, mapped_column from sqlalchemy_utils import TSVectorType from db import lexicon_term @@ -86,7 +84,7 @@ class Thing(Base, AutoBaseMixin, ReleaseMixin): ) -class ThingIdLink(Base, AutoBaseMixin): +class ThingIdLink(Base, AutoBaseMixin, ReleaseMixin): """ Represents a link associated with a Thing. """ @@ -99,7 +97,7 @@ class ThingIdLink(Base, AutoBaseMixin): thing = relationship("Thing", backref="links") -class WellScreen(Base, AutoBaseMixin): +class WellScreen(Base, AutoBaseMixin, ReleaseMixin): thing_id = Column( Integer, ForeignKey("thing.id", ondelete="CASCADE"), nullable=False ) diff --git a/schemas/__init__.py b/schemas/__init__.py index 21b9355ba..698dc02dd 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -20,15 +20,15 @@ class ResourceNotFoundResponse(BaseModel): detail: str -class BaseCreateSchema(BaseModel): +class BaseCreateModel(BaseModel): release_status: str -class BaseUpdateSchema(BaseCreateSchema): +class BaseUpdateModel(BaseCreateModel): release_status: str | None = None -class BaseResponseSchema(BaseModel): +class BaseResponseModel(BaseModel): id: int # every ORM model should have an id field created_at: AwareDatetime release_status: str diff --git a/schemas/asset.py b/schemas/asset.py index de8b37c10..c62ab68a5 100644 --- a/schemas/asset.py +++ b/schemas/asset.py @@ -15,7 +15,7 @@ # =============================================================================== from pydantic import BaseModel -from schemas import ORMBaseModel +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel class BaseAsset(BaseModel): @@ -28,18 +28,18 @@ class BaseAsset(BaseModel): # -------- CREATE ---------- -class CreateAsset(BaseAsset): +class CreateAsset(BaseCreateModel, BaseAsset): thing_id: int | None = None # -------- RESPONSE -------- -class AssetResponse(ORMBaseModel, BaseAsset): +class AssetResponse(BaseResponseModel, BaseAsset): storage_service: str signed_url: str | None = None # -------- UPDATE ---------- -class UpdateAsset(BaseModel): +class UpdateAsset(BaseUpdateModel): name: str | None = None label: str | None = None diff --git a/schemas/contact.py b/schemas/contact.py index c8ff04439..454af0366 100644 --- a/schemas/contact.py +++ b/schemas/contact.py @@ -20,7 +20,7 @@ from phonenumbers import NumberParseException from pydantic import field_validator, BaseModel -from schemas import ORMBaseModel +from schemas import BaseResponseModel, BaseCreateModel, BaseUpdateModel from schemas.thing import ThingResponse @@ -65,7 +65,7 @@ def validate_phone(cls, phone_number_str: str | None) -> str | None: # -------- CREATE ---------- -class CreateEmail(ValidateEmail): +class CreateEmail(BaseCreateModel, ValidateEmail): """ Schema for creating an email. """ @@ -75,7 +75,7 @@ class CreateEmail(ValidateEmail): email_type: str = "Primary" # Default to 'Primary' -class CreatePhone(ValidatePhone): +class CreatePhone(BaseCreateModel, ValidatePhone): """ Schema for creating a phone number. """ @@ -85,7 +85,7 @@ class CreatePhone(ValidatePhone): phone_type: str = "Primary" # Default to 'Primary' -class CreateAddress(BaseModel): +class CreateAddress(BaseCreateModel): """ Schema for creating an address. """ @@ -102,16 +102,16 @@ class CreateAddress(BaseModel): address_type: str = "Primary" -class CreateThingAssociation(BaseModel): - """ - Schema for creating a ContactThingAssociation - """ +# class CreateThingAssociation(BaseModel): +# """ +# Schema for creating a ContactThingAssociation +# """ - contact_id: int - thing_id: int +# contact_id: int +# thing_id: int -class CreateContact(BaseModel): +class CreateContact(BaseCreateModel): """ Schema for creating a contact. """ @@ -131,8 +131,7 @@ class CreateContact(BaseModel): # -------- RESPONSE ---------- -class BaseItemResponse(ORMBaseModel): - id: int +class BaseItemResponse(BaseResponseModel): contact_id: int @@ -168,12 +167,11 @@ class AddressResponse(BaseItemResponse): address_type: str -class ContactResponse(ORMBaseModel): +class ContactResponse(BaseResponseModel): """ Response schema for contact details. """ - id: int name: str role: str emails: List[EmailResponse] = [] @@ -182,18 +180,18 @@ class ContactResponse(ORMBaseModel): things: List[ThingResponse] = [] # List of related things -class ThingContactAssociationResponse(ORMBaseModel): - """ - Response schema for thing-contact association details. - """ +# class ThingContactAssociationResponse(BaseUpdateModel): +# """ +# Response schema for thing-contact association details. +# """ - id: int - thing_id: int - contact_id: int +# id: int +# thing_id: int +# contact_id: int # -------- UPDATE ---------- -class UpdateContact(BaseModel): +class UpdateContact(BaseUpdateModel): """ Schema for updating contact information. """ @@ -206,7 +204,7 @@ class UpdateContact(BaseModel): # address: str | None = None -class UpdateEmail(ValidateEmail): +class UpdateEmail(BaseUpdateModel, ValidateEmail): """ Schema for updating email information. """ @@ -216,7 +214,7 @@ class UpdateEmail(ValidateEmail): email_type: str | None = None -class UpdatePhone(ValidatePhone): +class UpdatePhone(BaseUpdateModel, ValidatePhone): """ Schema for updating phone information. """ @@ -226,7 +224,7 @@ class UpdatePhone(ValidatePhone): phone_type: str | None = None -class UpdateAddress(BaseModel): +class UpdateAddress(BaseUpdateModel): """ Schema for updating address information. """ @@ -241,13 +239,13 @@ class UpdateAddress(BaseModel): address_type: str | None = None -class UpdateThingContactAssociation(BaseModel): - """ - Schema for updating thing-contact association information. - """ +# class UpdateThingContactAssociation(BaseUpdateModel): +# """ +# Schema for updating thing-contact association information. +# """ - thing_id: int | None = None - contact_id: int | None = None +# thing_id: int | None = None +# contact_id: int | None = None # ============= EOF ============================================= diff --git a/schemas/group.py b/schemas/group.py index c5f525748..49c3a25a4 100644 --- a/schemas/group.py +++ b/schemas/group.py @@ -18,7 +18,7 @@ from pydantic import BaseModel, field_validator, model_validator from typing_extensions import Self -from schemas import ORMBaseModel +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel from services.validation.geospatial import validate_wkt_geometry @@ -37,7 +37,7 @@ def validate_area_is_wkt(cls, wkt): # -------- CREATE ---------- -class CreateGroup(ValidateGroup): +class CreateGroup(BaseCreateModel, ValidateGroup): """ Schema for creating a group. """ @@ -46,7 +46,7 @@ class CreateGroup(ValidateGroup): # -------- RESPONSE -------- -class GroupResponse(ORMBaseModel): +class GroupResponse(BaseResponseModel): """ Pydantic model for the response of a group. This model can be extended to include additional fields as needed. @@ -65,7 +65,7 @@ def project_area_to_wkt(self: Self) -> Self: # -------- UPDATE ---------- -class UpdateGroup(ValidateGroup): +class UpdateGroup(BaseUpdateModel, ValidateGroup): """ Pydantic model for updating a group. This model can be extended to include additional fields as needed. diff --git a/schemas/lexicon.py b/schemas/lexicon.py index 172385a35..de215ad03 100644 --- a/schemas/lexicon.py +++ b/schemas/lexicon.py @@ -13,11 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, AwareDatetime from typing import List -from schemas import ORMBaseModel - # -------- CREATE ---------- class CreateLexiconCategory(BaseModel): @@ -74,7 +72,17 @@ class UpdateLexiconTriple(BaseModel): # -------- RESPONSE ---------- -class LexiconCategoryResponse(ORMBaseModel): +class BaseLexiconResponse(BaseModel): + id: int + created_at: AwareDatetime + + model_config = ConfigDict( + from_attributes=True, + populate_by_name=True, + ) + + +class LexiconCategoryResponse(BaseLexiconResponse): """ Pydantic model for the response of a lexicon category. This model can be extended to include additional fields as needed. @@ -85,7 +93,7 @@ class LexiconCategoryResponse(ORMBaseModel): # terms: list[LexiconTermResponse] | None = None -class LexiconTermResponse(ORMBaseModel): +class LexiconTermResponse(BaseLexiconResponse): """ Pydantic model for the response of a lexicon term. This model can be extended to include additional fields as needed. @@ -96,7 +104,7 @@ class LexiconTermResponse(ORMBaseModel): categories: List[LexiconCategoryResponse] = [] -class LexiconTripleResponse(ORMBaseModel): +class LexiconTripleResponse(BaseLexiconResponse): subject: str predicate: str object_: str diff --git a/schemas/location.py b/schemas/location.py index ed23fdf63..aa8074e8c 100644 --- a/schemas/location.py +++ b/schemas/location.py @@ -17,7 +17,7 @@ from geoalchemy2.shape import to_shape from pydantic import BaseModel, field_validator -from schemas import ORMBaseModel +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel from services.validation.geospatial import validate_wkt_geometry """ @@ -29,7 +29,7 @@ # -------- CREATE ---------- -class CreateLocation(BaseModel): +class CreateLocation(BaseCreateModel): """ Schema for creating a sample location. """ @@ -55,12 +55,11 @@ class CreateGroupThing(BaseModel): # -------- RESPONSE ---------- -class LocationResponse(ORMBaseModel): +class LocationResponse(BaseResponseModel): """ Response schema for sample location details. """ - id: int name: str | None = None point: str release_status: str @@ -77,18 +76,17 @@ def point_to_wkt(cls, value): return None -class GroupLocationResponse(ORMBaseModel): +class GroupLocationResponse(BaseResponseModel): """ Response schema for group location details. """ - id: int group_id: int location_id: int # -------- UPDATE ---------- -class UpdateLocation(BaseModel): +class UpdateLocation(BaseUpdateModel): """ Schema for updating a location. """ @@ -96,14 +94,6 @@ class UpdateLocation(BaseModel): name: str | None = None notes: str | None = None point: str | None = None - release_status: str | None = None - - -class UpdateGroup(BaseModel): - name: str - description: str | None = None - parent_group_id: int | None = None - project_area: str | None = None # ============= EOF ============================================= diff --git a/schemas/observation.py b/schemas/observation.py index 4fb0474f3..a5269f91d 100644 --- a/schemas/observation.py +++ b/schemas/observation.py @@ -24,7 +24,7 @@ from typing import Annotated from typing_extensions import Self -from schemas import ORMBaseModel +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel # class GeothermalMixin: @@ -67,7 +67,7 @@ def prepend_observed_property(self: Self) -> Self: # -------- CREATE ---------- -class CreateBaseObservation(ValidateObservation): +class CreateBaseObservation(BaseCreateModel, ValidateObservation): observation_datetime: Annotated[AwareDatetime, PastDatetime()] sample_id: int | None = None sensor_id: int @@ -95,7 +95,7 @@ class CreateGeothermalObservation(CreateBaseObservation): # -------- UPDATE ------------ -class UpdateBaseObservation(ValidateObservation): +class UpdateBaseObservation(BaseUpdateModel, ValidateObservation): observation_datetime: Annotated[AwareDatetime, PastDatetime()] | None = None sample_id: int | None = None sensor_id: int | None = None @@ -121,7 +121,7 @@ class UpdateGeothermalObservation(UpdateBaseObservation): # -------- RESPONSE ---------- -class BaseObservationResponse(ORMBaseModel): +class BaseObservationResponse(BaseResponseModel): sample_id: int sensor_id: int observation_datetime: AwareDatetime diff --git a/schemas/sample.py b/schemas/sample.py index 7451ad522..8ed753deb 100644 --- a/schemas/sample.py +++ b/schemas/sample.py @@ -24,7 +24,7 @@ from typing import Annotated from typing_extensions import Self -from schemas import ORMBaseModel +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel from schemas.thing import ThingResponse """ @@ -85,12 +85,11 @@ def convert_sample_date_to_utc(sample_date: AwareDatetime) -> AwareDatetime: # -------- CREATE ---------- -class CreateSample(ValidateSample): +class CreateSample(BaseCreateModel, ValidateSample): thing_id: int sample_type: str field_sample_id: str sample_date: Annotated[AwareDatetime, PastDatetime()] - release_status: str sampler_name: str # REFACTOR TODO: update with enum/restricted values qc_sample: str = "Original" @@ -111,20 +110,19 @@ class CreateSample(ValidateSample): # -------- UPDATE ---------- -class UpdateSample(ValidateSample): +class UpdateSample(BaseUpdateModel, ValidateSample): """ Development notes: setting = None makes the field optional, but if it is defined it must be of that type. """ - thing_id: int = None # REFACTOR TODO: should users be able to change this? - sample_type: str = None - field_sample_id: str = None - sample_date: Annotated[AwareDatetime, PastDatetime()] = None - release_status: str = None - sampler_name: str = None # REFACTOR TODO: update with enum/restricted values - qc_sample: str = None + thing_id: int | None = None # REFACTOR TODO: should users be able to change this? + sample_type: str | None = None + field_sample_id: str | None = None + sample_date: Annotated[AwareDatetime, PastDatetime()] | None = None + sampler_name: str | None = None # REFACTOR TODO: update with enum/restricted values + qc_sample: str | None = None sensor_id: int | None = None # REFACTOR TODO: should users be able to change this? sample_matrix: str | None = ( @@ -143,7 +141,7 @@ class UpdateSample(ValidateSample): # -------- RESPONSE ---------- -class SampleResponse(ORMBaseModel): +class SampleResponse(BaseResponseModel): thing: ThingResponse sample_type: str field_sample_id: str diff --git a/schemas/sensor.py b/schemas/sensor.py index e412895a3..6dbcf9f19 100644 --- a/schemas/sensor.py +++ b/schemas/sensor.py @@ -15,7 +15,6 @@ # =============================================================================== from typing_extensions import Annotated, Self from datetime import timezone - from pydantic import ( BaseModel, AwareDatetime, @@ -24,6 +23,8 @@ field_validator, ) +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel + # ------- VALIDATION ------ @@ -50,7 +51,7 @@ def check_datetime_values(self) -> Self: # -------- CREATE ---------- -class CreateSensor(ValidateSensor): +class CreateSensor(BaseCreateModel, ValidateSensor): """ Schema for creating a new sensor. """ @@ -66,7 +67,7 @@ class CreateSensor(ValidateSensor): # -------- UPDATE ---------- -class UpdateSensor(ValidateSensor): +class UpdateSensor(BaseUpdateModel, ValidateSensor): name: str | None = None model: str | None = None serial_no: str | None = None @@ -77,8 +78,7 @@ class UpdateSensor(ValidateSensor): # -------- RESPONSE ---------- -class SensorResponse(BaseModel): - id: int +class SensorResponse(BaseResponseModel): name: str model: str | None # = Column(String(50)) serial_no: str | None # = Column(String(50)) diff --git a/schemas/thing.py b/schemas/thing.py index 1e2cd7503..2ac3d72ba 100644 --- a/schemas/thing.py +++ b/schemas/thing.py @@ -17,7 +17,7 @@ from pydantic import BaseModel, model_validator -from schemas import ORMBaseModel +from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel from schemas.location import LocationResponse @@ -33,7 +33,7 @@ class CreateThingIdLink(BaseModel): alternate_organization: str -class CreateBaseThing(BaseModel): +class CreateBaseThing(BaseCreateModel): """ Developer's notes @@ -46,7 +46,6 @@ class CreateBaseThing(BaseModel): location_id: int | None = None # Optional location ID for the thing group_id: int | None = None # Optional group ID for the thing name: str # Name of the thing - release_status: str # Release status of the thing class CreateWell(CreateBaseThing): @@ -68,13 +67,7 @@ class CreateSpring(CreateBaseThing): spring_type: str | None = None -class CreateThing(CreateWell, CreateSpring): - """ - Schema for creating a thing. - """ - - -class CreateWellScreen(BaseModel): +class CreateWellScreen(BaseCreateModel): """ Schema for creating a well screen. """ @@ -96,10 +89,9 @@ def check_depths(self): # ------ RESPONSE ---------- -class BaseThingResponse(ORMBaseModel): +class BaseThingResponse(BaseResponseModel): name: str thing_type: str - release_status: str active_location: LocationResponse | None = None @@ -131,7 +123,7 @@ class ThingResponse(WellResponse, SpringResponse): pass -class ThingIdLinkResponse(ORMBaseModel): +class ThingIdLinkResponse(BaseResponseModel): thing_id: int thing: ThingResponse relation: str @@ -147,7 +139,7 @@ class LocationWellResponse(LocationResponse): well: List[WellResponse] = [] # List of wells associated with the sample location -class WellScreenResponse(ORMBaseModel): +class WellScreenResponse(BaseResponseModel): """ Response schema for well screen details. """ @@ -194,13 +186,12 @@ class FeatureCollectionResponse(BaseModel): # -------- UPDATE ------------ -class UpdateThing(BaseModel): +class UpdateThing(BaseUpdateModel): """ Schema for updating a thing. """ name: str | None = None # Optional name for the thing - release_status: str | None = None class UpdateWell(UpdateThing): @@ -215,13 +206,13 @@ class UpdateSpring(UpdateThing): spring_type: str | None = None -class UpdateThingIdLink(BaseModel): +class UpdateThingIdLink(BaseUpdateModel): alternate_organization: str | None = None alternate_id: str | None = None relation: str | None = None -class UpdateWellScreen(BaseModel): +class UpdateWellScreen(BaseUpdateModel): screen_depth_bottom: float | None = None screen_depth_top: float | None = None screen_description: str | None = None diff --git a/tests/conftest.py b/tests/conftest.py index 3a0cbe041..6a167ad09 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,6 +65,7 @@ def well_screen(water_well_thing): screen_depth_bottom=20.0, screen_type="PVC", screen_description="Test well screen description", + release_status="draft", ) session.add(screen) session.commit() @@ -80,6 +81,7 @@ def second_well_screen(water_well_thing): screen_depth_bottom=40.0, screen_type="PVC", screen_description="Test well screen description", + release_status="private", ) session.add(screen) session.commit() @@ -96,6 +98,7 @@ def thing_id_link(water_well_thing): relation="same_as", alternate_id="4321-1234", alternate_organization="USGS", + release_status="private", ) session.add(id_link) session.commit() @@ -110,6 +113,7 @@ def second_thing_id_link(water_well_thing): relation="same_as", alternate_id="4321-1234", alternate_organization="USGS", + release_status="private", ) session.add(id_link) session.commit() @@ -173,6 +177,7 @@ def sensor(): datetime_removed="2023-01-02T00:00:00Z", recording_interval=60, notes="Test equipment", + release_status="draft", ) session.add(sensor) session.commit() @@ -190,6 +195,7 @@ def second_sensor(): datetime_removed="2023-01-02T00:00:00Z", recording_interval=60, notes="Test equipment", + release_status="draft", ) session.add(sensor) session.commit() @@ -250,6 +256,7 @@ def second_sample(water_well_thing, sensor): def contact(water_well_thing): with session_ctx() as session: contact = Contact( + release_status="private", name="Test Contact", role="Owner", ) @@ -271,6 +278,7 @@ def contact(water_well_thing): def address(contact): with session_ctx() as session: address = Address( + release_status="private", address_line_1="123 Main St", address_line_2="Apt 4B", city="Test City", @@ -290,7 +298,10 @@ def address(contact): def email(contact): with session_ctx() as session: email = Email( - email="test@example.com", email_type="Primary", contact_id=contact.id + email="test@example.com", + email_type="Primary", + contact_id=contact.id, + release_status="private", ) session.add(email) session.commit() @@ -302,7 +313,10 @@ def email(contact): def phone(contact): with session_ctx() as session: phone = Phone( - phone_number="+15051234567", phone_type="Mobile", contact_id=contact.id + phone_number="+15051234567", + phone_type="Mobile", + contact_id=contact.id, + release_status="private", ) session.add(phone) session.commit() @@ -314,6 +328,7 @@ def phone(contact): def second_contact(): with session_ctx() as session: contact = Contact( + release_status="private", name="Test Second Contact", role="Owner", ) @@ -334,6 +349,7 @@ def second_email(second_contact): email="testsecondcontact@gmail.com", email_type="Primary", contact_id=second_contact.id, + release_status="private", ) session.add(email) session.commit() @@ -350,6 +366,7 @@ def second_phone(second_contact): phone_number="123-456-7890", phone_type="Primary", contact_id=second_contact.id, + release_status="private", ) session.add(phone) session.commit() @@ -363,6 +380,7 @@ def second_phone(second_contact): def second_address(second_contact): with session_ctx() as session: address = Address( + release_status="private", address_line_1="456 Secondary St", address_line_2="Apt 12A", city="Test Metropolis", @@ -384,6 +402,7 @@ def second_address(second_contact): def asset(): with session_ctx() as session: asset = Asset( + release_status="draft", name="Test Asset", label="test label", mime_type="image/png", @@ -402,6 +421,7 @@ def asset(): def asset_with_associated_thing(water_well_thing): with session_ctx() as session: asset = Asset( + release_status="draft", name="Test Asset with water_well_thing", label="test label", mime_type="application/pdf", @@ -522,6 +542,7 @@ def observation_to_delete(sample, sensor): def group(water_well_thing): with session_ctx() as session: group = Group( + release_status="draft", name="Test Group", description="This is a test group.", project_area="MULTIPOLYGON(((-107.2 33.6, -106.6 33.6, -106.6 34.2, -107.2 34.2, -107.2 33.6)))", @@ -545,6 +566,7 @@ def group(water_well_thing): def second_group(water_well_thing): with session_ctx() as session: group = Group( + release_status="draft", name="Second Test Group", description="This is a second test group.", project_area="MULTIPOLYGON(((-107.2 33.6, -106.6 33.6, -106.6 34.2, 0 0, -107.2 34.2, -107.2 33.6)))", diff --git a/tests/test_asset.py b/tests/test_asset.py index 7cce58112..094bbf0fa 100644 --- a/tests/test_asset.py +++ b/tests/test_asset.py @@ -88,6 +88,7 @@ def test_upload_asset(): def test_add_asset(water_well_thing): payload = { + "release_status": "draft", "thing_id": water_well_thing.id, "name": "test_asset.png", "label": "Test Asset", @@ -102,6 +103,7 @@ def test_add_asset(water_well_thing): data = resp.json() assert "id" in data assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["name"] == payload["name"] assert data["label"] == payload["label"] assert data["uri"] == payload["uri"] @@ -125,6 +127,7 @@ def test_add_asset_409_bad_thing_id(water_well_thing): "storage_path": "mock/path/to/asset/test_asset.png", "mime_type": "image/png", "size": 12345, + "release_status": "draft", } resp = client.post("/asset", json=payload) assert resp.status_code == 409 @@ -147,6 +150,7 @@ def test_get_assets(asset, asset_with_associated_thing): assert data["items"][0]["created_at"] == asset.created_at.isoformat().replace( "+00:00", "Z" ) + assert data["items"][0]["release_status"] == asset.release_status assert data["items"][0]["name"] == asset.name assert data["items"][0]["label"] == asset.label assert data["items"][0]["storage_path"] == asset.storage_path @@ -157,6 +161,22 @@ def test_get_assets(asset, asset_with_associated_thing): assert data["items"][0]["signed_url"] == None assert data["items"][1]["id"] == asset_with_associated_thing.id + assert data["items"][1][ + "created_at" + ] == asset_with_associated_thing.created_at.isoformat().replace("+00:00", "Z") + assert ( + data["items"][1]["release_status"] == asset_with_associated_thing.release_status + ) + assert data["items"][1]["name"] == asset_with_associated_thing.name + assert data["items"][1]["label"] == asset_with_associated_thing.label + assert data["items"][1]["storage_path"] == asset_with_associated_thing.storage_path + assert data["items"][1]["mime_type"] == asset_with_associated_thing.mime_type + assert data["items"][1]["size"] == asset_with_associated_thing.size + assert data["items"][1]["uri"] == asset_with_associated_thing.uri + assert ( + data["items"][1]["storage_service"] + == asset_with_associated_thing.storage_service + ) assert data["items"][1]["signed_url"] == None @@ -180,6 +200,7 @@ def test_get_asset_by_id(asset): data = response.json() assert data["id"] == asset.id assert data["created_at"] == asset.created_at.isoformat().replace("+00:00", "Z") + assert data["release_status"] == asset.release_status assert data["name"] == asset.name assert data["label"] == asset.label assert data["storage_path"] == asset.storage_path @@ -202,13 +223,18 @@ def test_get_asset_by_id_404_not_found(asset): def test_patch_asset(asset): - payload = {"name": "patched name", "label": "patched label"} + payload = { + "name": "patched name", + "label": "patched label", + "release_status": "public", + } response = client.patch(f"/asset/{asset.id}", json=payload) assert response.status_code == 200 data = response.json() assert data["id"] == asset.id assert data["name"] == payload["name"] assert data["label"] == payload["label"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Asset, payload, asset) diff --git a/tests/test_contact.py b/tests/test_contact.py index 33a9ee1b0..1173a702d 100644 --- a/tests/test_contact.py +++ b/tests/test_contact.py @@ -61,13 +61,27 @@ def test_validate_email(): def test_add_contact(spring_thing): payload = { + "release_status": "private", "name": "Test Contact 2", "role": "Owner", "thing_id": spring_thing.id, - "emails": [{"email": "testcontact2@gmail.com", "email_type": "Primary"}], - "phones": [{"phone_number": "+14153334444", "phone_type": "Primary"}], + "emails": [ + { + "email": "testcontact2@gmail.com", + "email_type": "Primary", + "release_status": "private", + } + ], + "phones": [ + { + "phone_number": "+14153334444", + "phone_type": "Primary", + "release_status": "private", + } + ], "addresses": [ { + "release_status": "private", "address_line_1": "123 Default St", "address_line_2": "Apt 8R", "city": "Test Metropolis", @@ -80,22 +94,33 @@ def test_add_contact(spring_thing): } response = client.post("/contact", json=payload) data = response.json() + assert response.status_code == 201 assert "id" in data + assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["name"] == payload["name"] assert data["role"] == payload["role"] assert len(data["emails"]) == 1 + assert "id" in data["emails"][0] + assert "created_at" in data["emails"][0] assert data["emails"][0]["contact_id"] == data["id"] assert data["emails"][0]["email"] == payload["emails"][0]["email"] assert data["emails"][0]["email_type"] == payload["emails"][0]["email_type"] + assert data["emails"][0]["release_status"] == payload["emails"][0]["release_status"] assert len(data["phones"]) == 1 + assert "id" in data["phones"][0] + assert "created_at" in data["phones"][0] assert data["phones"][0]["contact_id"] == data["id"] assert data["phones"][0]["phone_number"] == payload["phones"][0]["phone_number"] assert data["phones"][0]["phone_type"] == payload["phones"][0]["phone_type"] + assert data["phones"][0]["release_status"] == payload["phones"][0]["release_status"] assert len(data["addresses"]) == 1 + assert "id" in data["addresses"][0] + assert "created_at" in data["addresses"][0] assert data["addresses"][0]["contact_id"] == data["id"] assert ( data["addresses"][0]["address_line_1"] @@ -112,6 +137,7 @@ def test_add_contact(spring_thing): assert ( data["addresses"][0]["address_type"] == payload["addresses"][0]["address_type"] ) + assert data["release_status"] == payload["release_status"] cleanup_post_test(Contact, data["id"]) @@ -119,13 +145,27 @@ def test_add_contact(spring_thing): def test_add_contact_409_bad_thing_id(): bad_thing_id = 9999 payload = { + "release_status": "private", "name": "Test Contact 3", "role": "Owner", "thing_id": bad_thing_id, - "emails": [{"email": "testcontact3@gmail.com", "email_type": "Primary"}], - "phones": [{"phone_number": "+14153334445", "phone_type": "Primary"}], + "emails": [ + { + "email": "testcontact3@gmail.com", + "email_type": "Primary", + "release_status": "private", + } + ], + "phones": [ + { + "phone_number": "+14153334445", + "phone_type": "Primary", + "release_status": "private", + } + ], "addresses": [ { + "release_status": "private", "address_line_1": "123 Default St", "address_line_2": "Apt 8R", "city": "Test Metropolis", @@ -152,11 +192,14 @@ def test_add_address(contact): "postal_code": "87502", "country": "United States", "address_type": "Primary", + "release_status": "draft", } response = client.post("/contact/address", json=payload) data = response.json() assert response.status_code == 201 assert "id" in data + assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["contact_id"] == contact.id assert data["address_line_1"] == payload["address_line_1"] assert data["address_line_2"] == payload["address_line_2"] @@ -180,6 +223,7 @@ def test_add_address_409_contact_not_found(contact): "postal_code": "87502", "country": "United States", "address_type": "Secondary", + "release_status": "draft", } response = client.post("/contact/address", json=payload) assert response.status_code == 409 @@ -195,14 +239,17 @@ def test_add_email(contact): "contact_id": contact.id, "email": "anothertestemail@nmt.edu", "email_type": "Primary", + "release_status": "draft", } response = client.post("/contact/email", json=payload) data = response.json() assert response.status_code == 201 assert "id" in data + assert "created_at" in data assert data["contact_id"] == contact.id assert data["email"] == payload["email"] assert data["email_type"] == payload["email_type"] + assert data["release_status"] == payload["release_status"] cleanup_post_test(Email, data["id"]) @@ -213,6 +260,7 @@ def test_add_email_409_contact_not_found(contact): "contact_id": bad_contact_id, "email": "anothertestemail@nmt.edu", "email_type": "Primary", + "release_status": "draft", } response = client.post("/contact/email", json=payload) assert response.status_code == 409 @@ -228,14 +276,17 @@ def test_add_phone(contact): "contact_id": contact.id, "phone_number": "+12345678901", "phone_type": "Primary", + "release_status": "draft", } response = client.post("/contact/phone", json=payload) data = response.json() assert response.status_code == 201 assert "id" in data + assert "created_at" in data assert data["contact_id"] == contact.id assert data["phone_number"] == payload["phone_number"] assert data["phone_type"] == payload["phone_type"] + assert data["release_status"] == payload["release_status"] cleanup_post_test(Phone, data["id"]) @@ -246,6 +297,7 @@ def test_add_phone_409_contact_not_found(contact): "contact_id": bad_contact_id, "phone_number": "+12345678901", "phone_type": "Primary", + "release_status": "draft", } response = client.post("/contact/phone", json=payload) assert response.status_code == 409 @@ -300,23 +352,38 @@ def test_get_contacts(contact, email, address, phone): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == contact.id + assert data["items"][0]["created_at"] == contact.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["name"] == contact.name assert data["items"][0]["role"] == contact.role + assert data["items"][0]["release_status"] == contact.release_status assert len(data["items"][0]["emails"]) == 1 assert data["items"][0]["emails"][0]["id"] == email.id + assert data["items"][0]["emails"][0][ + "created_at" + ] == email.created_at.isoformat().replace("+00:00", "Z") assert data["items"][0]["emails"][0]["contact_id"] == email.contact_id assert data["items"][0]["emails"][0]["email"] == email.email assert data["items"][0]["emails"][0]["email_type"] == email.email_type + assert data["items"][0]["emails"][0]["release_status"] == email.release_status assert len(data["items"][0]["phones"]) == 1 assert data["items"][0]["phones"][0]["id"] == phone.id + assert data["items"][0]["phones"][0][ + "created_at" + ] == phone.created_at.isoformat().replace("+00:00", "Z") assert data["items"][0]["phones"][0]["contact_id"] == phone.contact_id assert data["items"][0]["phones"][0]["phone_number"] == phone.phone_number assert data["items"][0]["phones"][0]["phone_type"] == phone.phone_type + assert data["items"][0]["phones"][0]["release_status"] == phone.release_status assert len(data["items"][0]["addresses"]) == 1 assert data["items"][0]["addresses"][0]["id"] == address.id + assert data["items"][0]["addresses"][0][ + "created_at" + ] == address.created_at.isoformat().replace("+00:00", "Z") assert data["items"][0]["addresses"][0]["contact_id"] == address.contact_id assert data["items"][0]["addresses"][0]["address_line_1"] == address.address_line_1 assert data["items"][0]["addresses"][0]["address_line_2"] == address.address_line_2 @@ -325,6 +392,7 @@ def test_get_contacts(contact, email, address, phone): assert data["items"][0]["addresses"][0]["postal_code"] == address.postal_code assert data["items"][0]["addresses"][0]["country"] == address.country assert data["items"][0]["addresses"][0]["address_type"] == address.address_type + assert data["items"][0]["addresses"][0]["release_status"] == address.release_status def test_get_contact_by_id(contact, email, address, phone): @@ -332,23 +400,36 @@ def test_get_contact_by_id(contact, email, address, phone): assert response.status_code == 200 data = response.json() assert data["id"] == contact.id + assert data["created_at"] == contact.created_at.isoformat().replace("+00:00", "Z") assert data["name"] == contact.name assert data["role"] == contact.role + assert data["release_status"] == contact.release_status assert len(data["emails"]) == 1 assert data["emails"][0]["id"] == email.id + assert data["emails"][0]["created_at"] == email.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["emails"][0]["contact_id"] == email.contact_id assert data["emails"][0]["email"] == email.email assert data["emails"][0]["email_type"] == email.email_type + assert data["emails"][0]["release_status"] == email.release_status assert len(data["phones"]) == 1 assert data["phones"][0]["id"] == phone.id + assert data["phones"][0]["created_at"] == phone.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["phones"][0]["contact_id"] == phone.contact_id assert data["phones"][0]["phone_number"] == phone.phone_number assert data["phones"][0]["phone_type"] == phone.phone_type + assert data["phones"][0]["release_status"] == phone.release_status assert len(data["addresses"]) == 1 assert data["addresses"][0]["id"] == address.id + assert data["addresses"][0]["created_at"] == address.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["addresses"][0]["contact_id"] == address.contact_id assert data["addresses"][0]["address_line_1"] == address.address_line_1 assert data["addresses"][0]["address_line_2"] == address.address_line_2 @@ -357,6 +438,7 @@ def test_get_contact_by_id(contact, email, address, phone): assert data["addresses"][0]["postal_code"] == address.postal_code assert data["addresses"][0]["country"] == address.country assert data["addresses"][0]["address_type"] == address.address_type + assert data["addresses"][0]["release_status"] == address.release_status def test_get_contact_by_id_404_not_found(contact): @@ -373,9 +455,13 @@ def test_get_contact_emails(contact, email): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == email.id + assert data["items"][0]["created_at"] == email.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["contact_id"] == email.contact_id assert data["items"][0]["email"] == email.email assert data["items"][0]["email_type"] == email.email_type + assert data["items"][0]["release_status"] == email.release_status def test_get_contact_emails_404_contact_not_found(contact, email): @@ -392,9 +478,13 @@ def test_get_contact_phones(contact, phone): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == phone.id + assert data["items"][0]["created_at"] == phone.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["contact_id"] == phone.contact_id assert data["items"][0]["phone_number"] == phone.phone_number assert data["items"][0]["phone_type"] == phone.phone_type + assert data["items"][0]["release_status"] == phone.release_status def test_get_contact_phones_404_contact_not_found(contact, phone): @@ -411,6 +501,9 @@ def test_get_contact_addresses(contact, address): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == address.id + assert data["items"][0]["created_at"] == address.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["contact_id"] == address.contact_id assert data["items"][0]["address_line_1"] == address.address_line_1 assert data["items"][0]["address_line_2"] == address.address_line_2 @@ -419,6 +512,7 @@ def test_get_contact_addresses(contact, address): assert data["items"][0]["postal_code"] == address.postal_code assert data["items"][0]["country"] == address.country assert data["items"][0]["address_type"] == address.address_type + assert data["items"][0]["release_status"] == address.release_status def test_get_contact_addresses_404_contact_not_found(contact, address): @@ -435,9 +529,13 @@ def test_get_emails(email): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == email.id + assert data["items"][0]["created_at"] == email.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["contact_id"] == email.contact_id assert data["items"][0]["email"] == email.email assert data["items"][0]["email_type"] == email.email_type + assert data["items"][0]["release_status"] == email.release_status def test_get_email_by_id(email): @@ -445,9 +543,11 @@ def test_get_email_by_id(email): assert response.status_code == 200 data = response.json() assert data["id"] == email.id + assert data["created_at"] == email.created_at.isoformat().replace("+00:00", "Z") assert data["contact_id"] == email.contact_id assert data["email"] == email.email assert data["email_type"] == email.email_type + assert data["release_status"] == email.release_status def test_get_email_404_not_found(email): @@ -464,9 +564,13 @@ def test_get_phones(phone): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == phone.id + assert data["items"][0]["created_at"] == phone.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["contact_id"] == phone.contact_id assert data["items"][0]["phone_number"] == phone.phone_number assert data["items"][0]["phone_type"] == phone.phone_type + assert data["items"][0]["release_status"] == phone.release_status def test_get_phone_by_id(phone): @@ -474,9 +578,19 @@ def test_get_phone_by_id(phone): assert response.status_code == 200 data = response.json() assert data["id"] == phone.id + assert data["created_at"] == phone.created_at.isoformat().replace("+00:00", "Z") assert data["contact_id"] == phone.contact_id assert data["phone_number"] == phone.phone_number assert data["phone_type"] == phone.phone_type + assert data["release_status"] == phone.release_status + + +def test_get_phone_404_not_found(): + bad_id = 99999 + response = client.get(f"/contact/phone/{bad_id}") + data = response.json() + assert response.status_code == 404 + assert data["detail"] == f"Phone with ID {bad_id} not found." def test_get_addresses(address): @@ -485,6 +599,9 @@ def test_get_addresses(address): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == address.id + assert data["items"][0]["created_at"] == address.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["contact_id"] == address.contact_id assert data["items"][0]["address_line_1"] == address.address_line_1 assert data["items"][0]["address_line_2"] == address.address_line_2 @@ -493,6 +610,7 @@ def test_get_addresses(address): assert data["items"][0]["postal_code"] == address.postal_code assert data["items"][0]["country"] == address.country assert data["items"][0]["address_type"] == address.address_type + assert data["items"][0]["release_status"] == address.release_status def test_get_address_by_id(address): @@ -500,6 +618,7 @@ def test_get_address_by_id(address): assert response.status_code == 200 data = response.json() assert data["id"] == address.id + assert data["created_at"] == address.created_at.isoformat().replace("+00:00", "Z") assert data["contact_id"] == address.contact_id assert data["address_line_1"] == address.address_line_1 assert data["address_line_2"] == address.address_line_2 @@ -508,6 +627,7 @@ def test_get_address_by_id(address): assert data["postal_code"] == address.postal_code assert data["country"] == address.country assert data["address_type"] == address.address_type + assert data["release_status"] == address.release_status def test_get_address_by_id_404_not_found(address): @@ -569,7 +689,7 @@ def test_get_address_by_id_404_not_found(address): def test_patch_contact(contact): - payload = {"name": "Updated Contact"} + payload = {"name": "Updated Contact", "release_status": "archived"} response = client.patch( f"/contact/{contact.id}", json=payload, @@ -577,8 +697,8 @@ def test_patch_contact(contact): assert response.status_code == 200 data = response.json() - assert data["id"] == contact.id assert data["name"] == payload["name"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Contact, payload, contact) @@ -597,14 +717,12 @@ def test_patch_contact_404_not_found(contact): def test_patch_email(email): - payload = {"email": "boo@bar.com"} + payload = {"email": "boo@bar.com", "release_status": "archived"} response = client.patch(f"/contact/email/{email.id}", json=payload) data = response.json() assert response.status_code == 200 - assert data["id"] == email.id - assert data["contact_id"] == email.contact_id assert data["email"] == payload["email"] - assert data["email_type"] == email.email_type + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Email, payload, email) @@ -619,14 +737,13 @@ def test_patch_email_404_not_found(email): def test_patch_phone(phone): - payload = {"phone_number": "+19709654321"} + payload = {"phone_number": "+19709654321", "release_status": "archived"} response = client.patch(f"/contact/phone/{phone.id}", json=payload) data = response.json() assert response.status_code == 200 assert data["id"] == phone.id - assert data["contact_id"] == phone.contact_id assert data["phone_number"] == payload["phone_number"] - assert data["phone_type"] == phone.phone_type + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Phone, payload, phone) @@ -642,6 +759,7 @@ def test_patch_phone_404_not_found(phone): def test_edit_address(address): payload = { + "release_status": "archived", "address_line_1": "456 Elm St", "address_line_2": "Apt 21B", "city": "Updated City", @@ -652,8 +770,6 @@ def test_edit_address(address): response = client.patch(f"/contact/address/{address.id}", json=payload) data = response.json() assert response.status_code == 200 - assert data["id"] == address.id - assert data["contact_id"] == address.contact_id assert data["address_line_1"] == payload["address_line_1"] assert data["address_line_2"] == payload["address_line_2"] assert data["city"] == payload["city"] @@ -661,6 +777,7 @@ def test_edit_address(address): assert data["postal_code"] == payload["postal_code"] assert data["country"] == payload["country"] assert data["address_type"] == address.address_type + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Address, payload, address) diff --git a/tests/test_group.py b/tests/test_group.py index 4b9095591..2b5be0d0b 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -60,6 +60,7 @@ def test_project_area_not_multipolygon(): def test_add_group(): payload = { + "release_status": "private", "name": "Test Group", "description": "This is a test group.", "project_area": "MULTIPOLYGON (((0 0, 1 1, 2 2, 3 3, 4 4, 1 2, 0 0)))", @@ -69,6 +70,7 @@ def test_add_group(): data = response.json() assert "id" in data assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["name"] == payload["name"] assert data["description"] == payload["description"] assert data["project_area"] == payload["project_area"] @@ -88,6 +90,7 @@ def test_get_groups(group): assert data["items"][0]["created_at"] == group.created_at.isoformat().replace( "+00:00", "Z" ) + assert data["items"][0]["release_status"] == group.release_status assert data["items"][0]["name"] == group.name assert data["items"][0]["project_area"] == to_shape(group.project_area).wkt assert data["items"][0]["description"] == group.description @@ -104,6 +107,7 @@ def test_get_group_by_id(group): assert data["project_area"] == to_shape(group.project_area).wkt assert data["description"] == group.description assert data["parent_group_id"] == group.parent_group_id + assert data["release_status"] == group.release_status def test_get_group_by_id_404_not_found(group): @@ -125,14 +129,13 @@ def test_get_group_things(): def test_patch_group(group): - payload = { - "name": "Updated Group", - } + payload = {"name": "Updated Group", "release_status": "private"} response = client.patch(f"/group/{group.id}", json=payload) assert response.status_code == 200 data = response.json() assert data["id"] == group.id assert data["name"] == payload["name"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Group, payload, group) diff --git a/tests/test_location.py b/tests/test_location.py index f1056d75a..6d4945949 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -51,6 +51,7 @@ def test_add_location(): assert response.status_code == 201 data = response.json() assert "id" in data + assert "created_at" in data assert data["name"] == payload["name"] assert data["point"] == payload["point"] assert data["release_status"] == payload["release_status"] @@ -106,6 +107,9 @@ def test_get_locations(location): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == location.id + assert data["items"][0]["created_at"] == location.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["name"] == location.name assert data["items"][0]["point"] == to_shape(location.point).wkt assert data["items"][0]["release_status"] == location.release_status @@ -116,6 +120,7 @@ def test_get_location_by_id(location): assert response.status_code == 200 data = response.json() assert data["id"] == location.id + assert data["created_at"] == location.created_at.isoformat().replace("+00:00", "Z") assert data["name"] == location.name assert data["point"] == to_shape(location.point).wkt assert data["release_status"] == location.release_status diff --git a/tests/test_observation.py b/tests/test_observation.py index 1200af45b..aa04e7cd9 100644 --- a/tests/test_observation.py +++ b/tests/test_observation.py @@ -56,6 +56,8 @@ def test_add_water_chemistry_observation(sample, sensor): data = response.json() assert response.status_code == 201 + assert "id" in data + assert "created_at" in data assert data["observation_datetime"] == payload["observation_datetime"] assert data["release_status"] == payload["release_status"] assert data["value"] == payload["value"] @@ -83,6 +85,8 @@ def test_add_groundwater_level_observation(sample, sensor): data = response.json() assert response.status_code == 201 + assert "id" in data + assert "created_at" in data assert data["observation_datetime"] == payload["observation_datetime"] assert data["release_status"] == payload["release_status"] assert data["value"] == payload["value"] @@ -113,6 +117,8 @@ def test_add_geothermal_observation(sample, sensor): data = response.json() assert response.status_code == 201 + assert "id" in data + assert "created_at" in data assert data["observation_datetime"] == payload["observation_datetime"] assert data["release_status"] == payload["release_status"] assert data["observation_depth"] == payload["observation_depth"] @@ -129,7 +135,7 @@ def test_add_geothermal_observation(sample, sensor): def test_patch_groundwater_level_observation(groundwater_level_observation): - payload = {"measuring_point_height": 3} + payload = {"measuring_point_height": 3, "release_status": "private"} response = client.patch( f"/observation/groundwater-level/{groundwater_level_observation.id}", json=payload, @@ -138,6 +144,7 @@ def test_patch_groundwater_level_observation(groundwater_level_observation): assert response.status_code == 200 assert data["measuring_point_height"] == payload["measuring_point_height"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Observation, payload, groundwater_level_observation) @@ -176,7 +183,7 @@ def test_patch_groundwater_level_observation_404_wrong_observation_class( def test_patch_water_chemistry_observation(water_chemistry_observation): - payload = {"value": 8} + payload = {"value": 8, "release_status": "private"} response = client.patch( f"/observation/water-chemistry/{water_chemistry_observation.id}", json=payload, @@ -185,6 +192,7 @@ def test_patch_water_chemistry_observation(water_chemistry_observation): assert response.status_code == 200 assert data["value"] == payload["value"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Observation, payload, water_chemistry_observation) @@ -219,13 +227,14 @@ def test_patch_water_chemistry_observation_404_wrong_observation_class( def test_patch_geothermal_observation(geothermal_observation): - payload = {"observation_depth": 4} + payload = {"observation_depth": 4, "release_status": "private"} response = client.patch( f"/observation/geothermal/{geothermal_observation.id}", json=payload ) assert response.status_code == 200 data = response.json() assert data["observation_depth"] == payload["observation_depth"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(Observation, payload, geothermal_observation) @@ -287,6 +296,8 @@ def test_get_observation_by_id( data = response.json() assert data["id"] == obs.id + assert data["created_at"] == obs.created_at.isoformat().replace("+00:00", "Z") + assert data["release_status"] == obs.release_status if obs.observed_property == "groundwater level:groundwater level": assert data["depth_to_water_bgs"] == obs.value - obs.measuring_point_height assert data["observation_depth"] is None @@ -316,6 +327,9 @@ def test_get_groundwater_level_observations( data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == groundwater_level_observation.id + assert data["items"][0][ + "created_at" + ] == groundwater_level_observation.created_at.isoformat().replace("+00:00", "Z") assert data["items"][0]["sample_id"] == groundwater_level_observation.sample_id assert data["items"][0]["sensor_id"] == groundwater_level_observation.sensor_id assert ( @@ -357,6 +371,9 @@ def test_get_groundwater_level_observation_by_id(groundwater_level_observation): assert response.status_code == 200 data = response.json() assert data["id"] == groundwater_level_observation.id + assert data[ + "created_at" + ] == groundwater_level_observation.created_at.isoformat().replace("+00:00", "Z") assert data["sample_id"] == groundwater_level_observation.sample_id assert data["sensor_id"] == groundwater_level_observation.sensor_id assert ( @@ -496,6 +513,9 @@ def test_get_water_chemistry_observations(water_chemistry_observation): assert data["items"][0][ "created_at" ] == water_chemistry_observation.created_at.isoformat().replace("+00:00", "Z") + assert ( + data["items"][0]["release_status"] == water_chemistry_observation.release_status + ) assert data["items"][0]["sample_id"] == water_chemistry_observation.sample_id assert data["items"][0]["sensor_id"] == water_chemistry_observation.sensor_id assert ( @@ -521,6 +541,7 @@ def test_get_water_chemistry_observation_by_id(water_chemistry_observation): assert data[ "created_at" ] == water_chemistry_observation.created_at.isoformat().replace("+00:00", "Z") + assert data["release_status"] == water_chemistry_observation.release_status assert data["sample_id"] == water_chemistry_observation.sample_id assert data["sensor_id"] == water_chemistry_observation.sensor_id assert ( @@ -573,6 +594,10 @@ def test_get_geothermal_observations(geothermal_observation): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == geothermal_observation.id + assert data["items"][0][ + "created_at" + ] == geothermal_observation.created_at.isoformat().replace("+00:00", "Z") + assert data["items"][0]["release_status"] == geothermal_observation.release_status assert data["items"][0]["sample_id"] == geothermal_observation.sample_id assert data["items"][0]["sensor_id"] == geothermal_observation.sensor_id assert ( @@ -600,6 +625,7 @@ def test_get_geothermal_observation_by_id(geothermal_observation): assert data["created_at"] == geothermal_observation.created_at.isoformat().replace( "+00:00", "Z" ) + assert data["release_status"] == geothermal_observation.release_status assert data["sample_id"] == geothermal_observation.sample_id assert data["sensor_id"] == geothermal_observation.sensor_id assert data["observation_datetime"] == geothermal_observation.observation_datetime diff --git a/tests/test_sample.py b/tests/test_sample.py index 4003d8f2e..ba452d24c 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -78,6 +78,8 @@ def test_add_sample(spring_thing, sensor): ) data = response.json() assert response.status_code == 201 + assert "id" in data + assert "created_at" in data assert data["thing"]["id"] == spring_thing.id assert data["sample_type"] == payload["sample_type"] assert data["field_sample_id"] == payload["field_sample_id"] @@ -173,6 +175,7 @@ def test_patch_sample(sample): "sampler_name": "test sample b", "sample_method": "continuous", "sample_date": "2025-01-02T00:00:00Z", + "release_status": "private", } response = client.patch(f"/sample/{sample.id}", json=payload) assert response.status_code == 200 @@ -182,6 +185,7 @@ def test_patch_sample(sample): assert data["sampler_name"] == payload["sampler_name"] assert data["sample_date"] == payload["sample_date"] assert data["sample_method"] == payload["sample_method"] + assert data["release_status"] == payload["release_status"] # rollback after updating the sample cleanup_patch_test(Sample, payload, sample) @@ -257,6 +261,9 @@ def test_get_samples(sample, water_well_thing): data = response.json() assert len(data["items"]) == 1 assert data["items"][0]["id"] == sample.id + assert data["items"][0]["created_at"] == sample.created_at.isoformat().replace( + "+00:00", "Z" + ) assert data["items"][0]["thing"]["id"] == water_well_thing.id assert data["items"][0]["sample_type"] == sample.sample_type assert data["items"][0]["field_sample_id"] == sample.field_sample_id @@ -280,6 +287,7 @@ def test_get_sample_by_id(sample, water_well_thing): assert response.status_code == 200 data = response.json() assert data["id"] == sample.id + assert data["created_at"] == sample.created_at.isoformat().replace("+00:00", "Z") assert data["thing"]["id"] == water_well_thing.id assert data["sample_type"] == sample.sample_type assert data["field_sample_id"] == sample.field_sample_id diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 7b514059b..0c9e5aab9 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -63,11 +63,14 @@ def test_add_sensor(): "datetime_removed": None, "recording_interval": 60, "notes": "Test equipment", + "release_status": "draft", } response = client.post("/sensor", json=payload) assert response.status_code == 201 data = response.json() assert "id" in data + assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["name"] == payload["name"] assert data["model"] == payload["model"] assert data["serial_no"] == payload["serial_no"] @@ -84,13 +87,18 @@ def test_add_sensor(): def test_patch_sensor(sensor): - payload = {"name": "patched name", "model": "patched model"} + payload = { + "name": "patched name", + "model": "patched model", + "release_status": "draft", + } response = client.patch(f"/sensor/{sensor.id}", json=payload) assert response.status_code == 200 data = response.json() assert data["id"] == sensor.id assert data["name"] == payload["name"] assert data["model"] == payload["model"] + assert data["release_status"] == payload["release_status"] # cleanup after patch test cleanup_patch_test(Sensor, payload, sensor) @@ -140,6 +148,10 @@ def test_get_sensors(sensor): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == sensor.id + assert data["items"][0]["created_at"] == sensor.created_at.isoformat().replace( + "+00:00", "Z" + ) + assert data["items"][0]["release_status"] == sensor.release_status assert data["items"][0]["name"] == sensor.name assert data["items"][0]["model"] == sensor.model assert data["items"][0]["serial_no"] == sensor.serial_no @@ -154,6 +166,8 @@ def test_get_sensor_by_id(sensor): assert response.status_code == 200 data = response.json() assert data["id"] == sensor.id + assert data["created_at"] == sensor.created_at.isoformat().replace("+00:00", "Z") + assert data["release_status"] == sensor.release_status assert data["name"] == sensor.name assert data["model"] == sensor.model assert data["serial_no"] == sensor.serial_no diff --git a/tests/test_thing.py b/tests/test_thing.py index a37ab3713..39d028195 100644 --- a/tests/test_thing.py +++ b/tests/test_thing.py @@ -187,6 +187,7 @@ def test_add_well_screen(water_well_thing): "screen_depth_top": 10.0, "screen_depth_bottom": 20.0, "screen_type": "PVC", + "release_status": "draft", } response = client.post("/thing/well-screen", json=payload) @@ -194,6 +195,7 @@ def test_add_well_screen(water_well_thing): data = response.json() assert "id" in data assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["thing_id"] == water_well_thing.id assert data["screen_depth_top"] == payload["screen_depth_top"] assert data["screen_depth_bottom"] == payload["screen_depth_bottom"] @@ -209,6 +211,7 @@ def test_add_well_screen_409_bad_thing_id(): "screen_depth_top": 10.0, "screen_depth_bottom": 20.0, "screen_type": "PVC", + "release_status": "draft", } response = client.post("/thing/well-screen", json=payload) assert response.status_code == 409 @@ -225,6 +228,7 @@ def test_well_add_well_screen_409_wrong_thing_type(spring_thing): "screen_depth_top": 10.0, "screen_depth_bottom": 20.0, "screen_type": "PVC", + "release_status": "draft", } response = client.post("/thing/well-screen", json=payload) assert response.status_code == 409 @@ -244,6 +248,7 @@ def test_add_well_screen_409_bad_screen_type(water_well_thing): "screen_depth_top": 10.0, "screen_depth_bottom": 20.0, "screen_type": "NotARealType", + "release_status": "draft", } response = client.post("/thing/well-screen", json=payload) @@ -264,12 +269,14 @@ def test_add_thing_link(spring_thing): "relation": "same_as", "alternate_id": "4321-1234", "alternate_organization": "USGS", + "release_status": "draft", } response = client.post("/thing/id-link", json=payload) assert response.status_code == 201 data = response.json() assert "id" in data assert "created_at" in data + assert data["release_status"] == payload["release_status"] assert data["thing_id"] == spring_thing.id assert data["relation"] == payload["relation"] assert data["alternate_id"] == payload["alternate_id"] @@ -285,6 +292,7 @@ def test_add_thing_id_link_409_bad_thing_id(): "relation": "same_as", "alternate_id": "4321-1234", "alternate_organization": "USGS", + "release_status": "draft", } response = client.post("/thing/id-link", json=payload) assert response.status_code == 409 @@ -420,6 +428,10 @@ def test_get_well_screens(well_screen): assert data["items"][0]["screen_depth_bottom"] == well_screen.screen_depth_bottom assert data["items"][0]["screen_type"] == well_screen.screen_type assert data["items"][0]["screen_description"] == well_screen.screen_description + assert data["items"][0]["release_status"] == well_screen.release_status + assert data["items"][0]["created_at"] == well_screen.created_at.isoformat().replace( + "+00:00", "Z" + ) def test_get_well_screen_by_id(well_screen): @@ -432,6 +444,10 @@ def test_get_well_screen_by_id(well_screen): assert data["screen_depth_bottom"] == well_screen.screen_depth_bottom assert data["screen_type"] == well_screen.screen_type assert data["screen_description"] == well_screen.screen_description + assert data["release_status"] == well_screen.release_status + assert data["created_at"] == well_screen.created_at.isoformat().replace( + "+00:00", "Z" + ) def test_get_well_screen_by_id_404_not_found(well_screen): @@ -484,6 +500,10 @@ def test_get_thing_id_links(thing_id_link): data = response.json() assert data["total"] == 1 assert data["items"][0]["id"] == thing_id_link.id + assert data["items"][0][ + "created_at" + ] == thing_id_link.created_at.isoformat().replace("+00:00", "Z") + assert data["items"][0]["release_status"] == thing_id_link.release_status assert data["items"][0]["thing_id"] == thing_id_link.thing_id assert data["items"][0]["relation"] == thing_id_link.relation assert data["items"][0]["alternate_id"] == thing_id_link.alternate_id @@ -498,6 +518,10 @@ def test_get_thing_id_link_by_id(thing_id_link): assert response.status_code == 200 data = response.json() assert data["id"] == thing_id_link.id + assert data["created_at"] == thing_id_link.created_at.isoformat().replace( + "+00:00", "Z" + ) + assert data["release_status"] == thing_id_link.release_status assert data["thing_id"] == thing_id_link.thing_id assert data["relation"] == thing_id_link.relation assert data["alternate_id"] == thing_id_link.alternate_id @@ -581,6 +605,7 @@ def test_get_thing_by_id(water_well_thing): assert data["created_at"] == water_well_thing.created_at.isoformat().replace( "+00:00", "Z" ) + assert data["release_status"] == water_well_thing.release_status assert data["name"] == water_well_thing.name assert data["thing_type"] == water_well_thing.thing_type assert data["release_status"] == water_well_thing.release_status @@ -762,6 +787,7 @@ def test_patch_thing_id_link(thing_id_link): "relation": "related_to", "alternate_id": "9999-8888", "alternate_organization": "TWDB", + "release_status": "draft", } response = client.patch(f"/thing/id-link/{thing_id_link.id}", json=payload) assert response.status_code == 200 @@ -769,6 +795,7 @@ def test_patch_thing_id_link(thing_id_link): assert data["relation"] == payload["relation"] assert data["alternate_id"] == payload["alternate_id"] assert data["alternate_organization"] == payload["alternate_organization"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(ThingIdLink, payload, thing_id_link) @@ -792,6 +819,7 @@ def test_patch_well_screen(well_screen): "screen_depth_top": 1, "screen_description": "patched screen description", "screen_type": "Steel", + "release_status": "draft", } response = client.patch(f"/thing/well-screen/{well_screen.id}", json=payload) assert response.status_code == 200 @@ -800,6 +828,7 @@ def test_patch_well_screen(well_screen): assert data["screen_depth_top"] == payload["screen_depth_top"] assert data["screen_description"] == payload["screen_description"] assert data["screen_type"] == data["screen_type"] + assert data["release_status"] == payload["release_status"] cleanup_patch_test(WellScreen, payload, well_screen) From 141ea7199f078d727ca6c806743c88b4ac8cb476 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 14:34:55 -0600 Subject: [PATCH 5/7] feat: ensure all thing endpoints return active_location --- api/thing.py | 5 ++- services/thing_helper.py | 22 +++++++++++- tests/conftest.py | 6 +++- tests/test_thing.py | 75 +++++++++++++++++++++++++++++++++++----- 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/api/thing.py b/api/thing.py index c38813750..f85684625 100644 --- a/api/thing.py +++ b/api/thing.py @@ -68,6 +68,7 @@ add_well_screen, get_db_things, get_thing_of_a_thing_type_by_id, + get_active_location, ) from services.lexicon_helper import get_terms_by_category @@ -315,7 +316,9 @@ async def get_thing_by_id( """ Retrieve a thing by ID from the database. """ - return simple_get_by_id(session, Thing, thing_id) + thing = simple_get_by_id(session, Thing, thing_id) + thing.active_location = get_active_location(session, thing) + return thing @router.get("/{thing_id}/id-link", summary="Get thing links by thing ID") diff --git a/services/thing_helper.py b/services/thing_helper.py index 445e1f091..5d88cd564 100644 --- a/services/thing_helper.py +++ b/services/thing_helper.py @@ -38,6 +38,21 @@ def wkb_to_geojson(wkb_element): return mapping(geom) +def get_active_location(session: Session, thing: Thing) -> Location | None: + """ + The following SQL query retrieves the active location associated with by + assuming that the latest effective_start is the active location. + """ + sql = ( + select(Location) + .join(LocationThingAssociation) + .where(LocationThingAssociation.thing_id == thing.id) + .order_by(LocationThingAssociation.effective_start.desc()) + ) + active_location = session.execute(sql).scalars().one_or_none() + return active_location + + def get_db_things( filter_, order, @@ -126,6 +141,7 @@ def get_thing_of_a_thing_type_by_id(session: Session, request: Request, thing_id verify_thing_type_correspondence(thing, request) + thing.active_location = get_active_location(session, thing) return thing @@ -174,6 +190,8 @@ def add_thing( except Exception as e: session.rollback() raise e + + thing.active_location = get_active_location(session, thing) return thing @@ -218,7 +236,9 @@ def patch_thing( verify_thing_type_correspondence(thing, request) - return model_patcher(session, Thing, thing_id, payload, user) + thing = model_patcher(session, Thing, thing_id, payload, user) + thing.active_location = get_active_location(session, thing) + return thing # ============= EOF ============================================= diff --git a/tests/conftest.py b/tests/conftest.py index 6a167ad09..fbb45e77d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,9 @@ @pytest.fixture(scope="session") def location(): with session_ctx() as session: - loc = Location(point="SRID=4326;POINT(0 0 0)") + loc = Location( + name="first location", release_status="draft", point="POINT(0 0 0)" + ) session.add(loc) session.commit() session.refresh(loc) @@ -51,8 +53,10 @@ def water_well_thing(location): assoc = LocationThingAssociation() assoc.location_id = location.id assoc.thing_id = water_well.id + assoc.effective_start = "2025-02-01T00:00:00Z" session.add(assoc) session.commit() + session.refresh(assoc) yield water_well diff --git a/tests/test_thing.py b/tests/test_thing.py index 39d028195..b9f7485bb 100644 --- a/tests/test_thing.py +++ b/tests/test_thing.py @@ -26,6 +26,7 @@ viewer_function, amp_viewer_function, ) +from schemas.location import LocationResponse @pytest.fixture(scope="module", autouse=True) @@ -78,6 +79,12 @@ def test_add_water_well(location, group): assert data["well_depth"] == payload["well_depth"] assert data["well_construction_notes"] == payload["well_construction_notes"] + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location + cleanup_post_test(Thing, data["id"]) @@ -142,6 +149,12 @@ def test_add_spring(location, group): assert data["release_status"] == payload["release_status"] assert data["spring_type"] == payload["spring_type"] + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location + cleanup_post_test(Thing, data["id"]) @@ -306,7 +319,7 @@ def test_add_thing_id_link_409_bad_thing_id(): # GET tests ==================================================================== -def test_get_water_wells(water_well_thing): +def test_get_water_wells(water_well_thing, location): response = client.get("/thing/water-well") assert response.status_code == 200 data = response.json() @@ -325,9 +338,14 @@ def test_get_water_wells(water_well_thing): data["items"][0]["well_construction_notes"] == water_well_thing.well_construction_notes ) + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["items"][0]["active_location"] == expected_location -def test_get_water_well_by_id(water_well_thing): +def test_get_water_well_by_id(water_well_thing, location): response = client.get(f"/thing/water-well/{water_well_thing.id}") assert response.status_code == 200 data = response.json() @@ -342,6 +360,11 @@ def test_get_water_well_by_id(water_well_thing): assert data["well_depth"] == water_well_thing.well_depth assert data["hole_depth"] == water_well_thing.hole_depth assert data["well_construction_notes"] == water_well_thing.well_construction_notes + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location def test_get_water_well_by_id_404_not_found(water_well_thing): @@ -366,7 +389,7 @@ def test_get_water_well_by_id_404_wrong_type(spring_thing): assert data["detail"][0]["input"] == {"thing_id": spring_thing.id} -def test_get_springs(spring_thing): +def test_get_springs(spring_thing, location): response = client.get("/thing/spring") assert response.status_code == 200 data = response.json() @@ -379,9 +402,14 @@ def test_get_springs(spring_thing): assert data["items"][0]["thing_type"] == spring_thing.thing_type assert data["items"][0]["release_status"] == spring_thing.release_status assert data["items"][0]["spring_type"] == spring_thing.spring_type + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["items"][0]["active_location"] == expected_location -def test_get_spring_by_id(spring_thing): +def test_get_spring_by_id(spring_thing, location): response = client.get(f"/thing/spring/{spring_thing.id}") assert response.status_code == 200 data = response.json() @@ -393,6 +421,11 @@ def test_get_spring_by_id(spring_thing): assert data["thing_type"] == spring_thing.thing_type assert data["release_status"] == spring_thing.release_status assert data["spring_type"] == spring_thing.spring_type + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location def test_get_spring_by_id_404_not_found(spring_thing): @@ -560,9 +593,15 @@ def test_get_thing_links_by_thing_id_404_not_found(water_well_thing, thing_id_li assert data["detail"] == f"Thing with ID {bad_id} not found." -def test_get_things(water_well_thing, spring_thing): +def test_get_things(water_well_thing, spring_thing, location): response = client.get("/thing") assert response.status_code == 200 + + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + data = response.json() assert data["total"] == 2 @@ -581,6 +620,7 @@ def test_get_things(water_well_thing, spring_thing): == water_well_thing.well_construction_notes ) assert data["items"][0]["spring_type"] is None + assert data["items"][0]["active_location"] == expected_location assert data["items"][1]["id"] == spring_thing.id assert data["items"][1][ @@ -594,9 +634,10 @@ def test_get_things(water_well_thing, spring_thing): assert data["items"][1]["well_depth"] is None assert data["items"][1]["hole_depth"] is None assert data["items"][1]["well_construction_notes"] is None + assert data["items"][1]["active_location"] == expected_location -def test_get_thing_by_id(water_well_thing): +def test_get_thing_by_id(water_well_thing, location): response = client.get(f"/thing/{water_well_thing.id}") assert response.status_code == 200 data = response.json() @@ -615,6 +656,12 @@ def test_get_thing_by_id(water_well_thing): assert data["well_construction_notes"] == water_well_thing.well_construction_notes assert data["spring_type"] is None + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location + def test_get_thing_by_id_404_not_found(water_well_thing): bad_id = 99999 @@ -676,7 +723,7 @@ def test_get_thing_by_id_404_not_found(water_well_thing): # PATCH tests ================================================================== -def test_patch_water_well(water_well_thing): +def test_patch_water_well(water_well_thing, location): payload = { "name": "patched water well", "release_status": "provisional", @@ -695,6 +742,12 @@ def test_patch_water_well(water_well_thing): assert data["hole_depth"] == payload["hole_depth"] assert data["well_construction_notes"] == payload["well_construction_notes"] + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location + cleanup_patch_test(Thing, payload, water_well_thing) @@ -735,7 +788,7 @@ def test_patch_water_well_404_wrong_type(spring_thing): assert data["detail"][0]["input"] == {"thing_id": spring_thing.id} -def test_patch_spring(spring_thing): +def test_patch_spring(spring_thing, location): payload = { "name": "patched spring", "release_status": "private", @@ -748,6 +801,12 @@ def test_patch_spring(spring_thing): assert data["release_status"] == payload["release_status"] assert data["spring_type"] == payload["spring_type"] + expected_location = LocationResponse.model_validate(location).model_dump() + expected_location["created_at"] = ( + expected_location["created_at"].isoformat().replace("+00:00", "Z") + ) + assert data["active_location"] == expected_location + cleanup_patch_test(Thing, payload, spring_thing) From 398cae6178abe7242984b575ee0401d67fc3f959 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 14:44:16 -0600 Subject: [PATCH 6/7] fix: close sessions in conftest --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index fbb45e77d..a86863c04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,6 @@ def water_well_thing(location): assoc.effective_start = "2025-02-01T00:00:00Z" session.add(assoc) session.commit() - session.refresh(assoc) yield water_well From 6b70cea9d5bfe4295c482ed4d8059a469c72fd1b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 2 Sep 2025 15:41:44 -0600 Subject: [PATCH 7/7] feat: ensure all endpoints are async --- api/contact.py | 24 ++++++++++++------------ api/group.py | 2 +- api/lexicon.py | 22 ++++++++++++---------- api/location.py | 4 ++-- api/observation.py | 30 +++++++++++++++--------------- api/sample.py | 10 +++++----- api/search.py | 4 ++-- api/sensor.py | 10 +++++----- api/thing.py | 16 ++++++++-------- 9 files changed, 62 insertions(+), 60 deletions(-) diff --git a/api/contact.py b/api/contact.py index a67e5a7b4..0b4c99782 100644 --- a/api/contact.py +++ b/api/contact.py @@ -119,7 +119,7 @@ def database_error_handler( summary="Create a new contact", status_code=status.HTTP_201_CREATED, ) -def create_contact( +async def create_contact( contact_data: CreateContact, session: session_dependency, user: amp_admin_dependency ) -> ContactResponse: try: @@ -133,7 +133,7 @@ def create_contact( summary="Add an address to a contact", status_code=status.HTTP_201_CREATED, ) -def create_address( +async def create_address( address_data: CreateAddress, session: session_dependency, user: amp_admin_dependency, @@ -156,7 +156,7 @@ def create_address( summary="Add an email to a contact", status_code=status.HTTP_201_CREATED, ) -def create_email( +async def create_email( email_data: CreateEmail, session: session_dependency, user: amp_admin_dependency, @@ -172,7 +172,7 @@ def create_email( summary="Add a phone number to a contact", status_code=status.HTTP_201_CREATED, ) -def create_phone( +async def create_phone( phone_data: CreatePhone, session: session_dependency, user: amp_admin_dependency, @@ -211,7 +211,7 @@ def create_phone( @router.patch( "/email/{email_id}", ) -def update_contact_email( +async def update_contact_email( email_id: int, email_data: UpdateEmail, session: session_dependency, @@ -226,7 +226,7 @@ def update_contact_email( @router.patch( "/phone/{phone_id}", ) -def update_contact_phone( +async def update_contact_phone( phone_id: int, phone_data: UpdatePhone, session: session_dependency, @@ -246,7 +246,7 @@ def update_contact_phone( @router.patch( "/address/{address_id}", ) -def update_contact_address( +async def update_contact_address( address_id: int, address_data: UpdateAddress, session: session_dependency, @@ -294,7 +294,7 @@ def update_contact_address( @router.patch("/{contact_id}", summary="Update contact") -def update_contact( +async def update_contact( contact_id: int, contact_data: UpdateContact, session: session_dependency, @@ -497,7 +497,7 @@ async def get_contact_addresses( @router.delete("/email/{email_id}", summary="Delete contact email") -def delete_contact_email( +async def delete_contact_email( email_id: int, session: session_dependency, user: amp_admin_dependency ): """ @@ -507,7 +507,7 @@ def delete_contact_email( @router.delete("/phone/{phone_id}", summary="Delete contact phone") -def delete_contact_phone( +async def delete_contact_phone( phone_id: int, session: session_dependency, user: amp_admin_dependency ): """ @@ -517,7 +517,7 @@ def delete_contact_phone( @router.delete("/address/{address_id}", summary="Delete contact address") -def delete_contact_address( +async def delete_contact_address( address_id: int, session: session_dependency, user: amp_admin_dependency ): """ @@ -542,7 +542,7 @@ def delete_contact_address( @router.delete("/{contact_id}", summary="Delete contact") -def delete_contact( +async def delete_contact( contact_id: int, session: session_dependency, user: amp_admin_dependency ): """ diff --git a/api/group.py b/api/group.py index 9e677ff24..a0983b0bf 100644 --- a/api/group.py +++ b/api/group.py @@ -40,7 +40,7 @@ @router.post("", summary="Create a new group", status_code=HTTP_201_CREATED) -def create_group( +async def create_group( group_data: CreateGroup, session: session_dependency, user: admin_dependency ) -> GroupResponse: """ diff --git a/api/lexicon.py b/api/lexicon.py index ea032a031..2cfd0d9fb 100644 --- a/api/lexicon.py +++ b/api/lexicon.py @@ -102,7 +102,7 @@ def database_error_handler( "/category", status_code=HTTP_201_CREATED, ) -def add_category( +async def add_category( category_data: CreateLexiconCategory, session: session_dependency, user: admin_dependency, @@ -118,7 +118,7 @@ def add_category( summary="Add term", status_code=HTTP_201_CREATED, ) -def add_term( +async def add_term( term_data: CreateLexiconTerm, session: session_dependency, user: admin_dependency ) -> LexiconTermResponse: """ @@ -135,7 +135,7 @@ def add_term( summary="Add triple", status_code=HTTP_201_CREATED, ) -def add_triple( +async def add_triple( triple_data: CreateLexiconTriple, session: session_dependency, user: admin_dependency, @@ -151,7 +151,7 @@ def add_triple( @router.patch("/term/{term_id}", status_code=HTTP_200_OK) -def update_lexicon_term( +async def update_lexicon_term( term_id: int, term_data: UpdateLexiconTerm, session: session_dependency, @@ -162,7 +162,7 @@ def update_lexicon_term( @router.patch("/category/{category_id}", status_code=HTTP_200_OK) -def update_lexicon_category( +async def update_lexicon_category( category_id: int, category_data: UpdateLexiconCategory, session: session_dependency, @@ -174,7 +174,7 @@ def update_lexicon_category( @router.patch("/triple/{triple_id}", status_code=HTTP_200_OK) -def update_lexicon_triple( +async def update_lexicon_triple( triple_id: int, triple_data: UpdateLexiconTriple, session: session_dependency, @@ -190,7 +190,7 @@ def update_lexicon_triple( @router.get("/term", summary="Get lexicon terms", status_code=HTTP_200_OK) -def get_lexicon_terms( +async def get_lexicon_terms( session: session_dependency, category: str | None = None, term: str | None = None, @@ -226,12 +226,14 @@ def get_lexicon_terms( @router.get("/term/{term_id}", status_code=HTTP_200_OK) -def get_lexicon_term(term_id: int, session: session_dependency) -> LexiconTermResponse: +async def get_lexicon_term( + term_id: int, session: session_dependency +) -> LexiconTermResponse: return simple_get_by_id(session, LexiconTerm, term_id) @router.get("/category") -def get_lexicon_categories( +async def get_lexicon_categories( session: session_dependency, sort: str = "name", order: str = "asc", @@ -244,7 +246,7 @@ def get_lexicon_categories( @router.get("/category/{category_id}") -def get_lexicon_category( +async def get_lexicon_category( category_id: int, session: session_dependency ) -> LexiconCategoryResponse: return simple_get_by_id(session, LexiconCategory, category_id) diff --git a/api/location.py b/api/location.py index 8241b1806..253663ab4 100644 --- a/api/location.py +++ b/api/location.py @@ -42,7 +42,7 @@ summary="Create a new sample location", status_code=status.HTTP_201_CREATED, ) -def create_location( +async def create_location( location_data: CreateLocation, session: session_dependency, user: admin_dependency ) -> LocationResponse: """ @@ -55,7 +55,7 @@ def create_location( "/{location_id}", summary="Update a location", ) -def update_location( +async def update_location( location_id: int, location_data: UpdateLocation, session: session_dependency, diff --git a/api/observation.py b/api/observation.py index 9493d4f7d..27da81974 100644 --- a/api/observation.py +++ b/api/observation.py @@ -51,7 +51,7 @@ # ============= Post ============================================= @router.post("/groundwater-level", status_code=HTTP_201_CREATED) -def add_groundwater_level_observation( +async def add_groundwater_level_observation( obs_data: CreateGroundwaterLevelObservation, session: session_dependency, user: amp_admin_dependency, @@ -63,7 +63,7 @@ def add_groundwater_level_observation( @router.post("/water-chemistry", status_code=HTTP_201_CREATED) -def add_water_chemistry_observation( +async def add_water_chemistry_observation( obs_data: CreateWaterChemistryObservation, session: session_dependency, user: amp_admin_dependency, @@ -76,7 +76,7 @@ def add_water_chemistry_observation( @router.post("/geothermal", status_code=HTTP_201_CREATED) -def add_geothermal_observation( +async def add_geothermal_observation( obs_data: CreateGeothermalObservation, session: session_dependency, user: admin_dependency, @@ -92,7 +92,7 @@ def add_geothermal_observation( @router.patch("/groundwater-level/{observation_id}", status_code=HTTP_200_OK) -def update_groundwater_level_observation( +async def update_groundwater_level_observation( observation_id: int, obs_data: UpdateGroundwaterLevelObservation, session: session_dependency, @@ -106,7 +106,7 @@ def update_groundwater_level_observation( @router.patch("/water-chemistry/{observation_id}", status_code=HTTP_200_OK) -def update_water_chemistry_observation( +async def update_water_chemistry_observation( observation_id: int, obs_data: UpdateWaterChemistryObservation, session: session_dependency, @@ -120,7 +120,7 @@ def update_water_chemistry_observation( @router.patch("/geothermal/{observation_id}", status_code=HTTP_200_OK) -def update_geothermal_observation( +async def update_geothermal_observation( observation_id: int, obs_data: UpdateGeothermalObservation, session: session_dependency, @@ -137,7 +137,7 @@ def update_geothermal_observation( @router.get("/groundwater-level", summary="Get groundwater level observations") -def get_groundwater_level_observations( +async def get_groundwater_level_observations( request: Request, session: session_dependency, user: amp_viewer_dependency, @@ -171,7 +171,7 @@ def get_groundwater_level_observations( "/groundwater-level/{observation_id}", summary="Get groundwater level observation by ID", ) -def get_groundwater_level_observation_by_id( +async def get_groundwater_level_observation_by_id( session: session_dependency, request: Request, user: amp_viewer_dependency, @@ -185,7 +185,7 @@ def get_groundwater_level_observation_by_id( @router.get("/water-chemistry", summary="Get water chemistry observations") -def get_water_chemistry_observations( +async def get_water_chemistry_observations( request: Request, session: session_dependency, user: amp_viewer_dependency, @@ -218,7 +218,7 @@ def get_water_chemistry_observations( @router.get( "/water-chemistry/{observation_id}", summary="Get water chemistry observation by ID" ) -def get_water_chemistry_observation_by_id( +async def get_water_chemistry_observation_by_id( session: session_dependency, request: Request, user: amp_viewer_dependency, @@ -232,7 +232,7 @@ def get_water_chemistry_observation_by_id( @router.get("/geothermal", summary="Get geothermal observations") -def get_geothermal_observations( +async def get_geothermal_observations( request: Request, session: session_dependency, user: viewer_dependency, @@ -263,7 +263,7 @@ def get_geothermal_observations( @router.get("/geothermal/{observation_id}", summary="Get geothermal observation by ID") -def get_geothermal_observation_by_id( +async def get_geothermal_observation_by_id( session: session_dependency, request: Request, user: amp_viewer_dependency, @@ -275,7 +275,7 @@ def get_geothermal_observation_by_id( @router.get("", summary="Get all observations") -def get_all_observations( +async def get_all_observations( request: Request, session: session_dependency, user: amp_viewer_dependency, @@ -303,7 +303,7 @@ def get_all_observations( @router.get("/{observation_id}", summary="Get an observation by its ID") -def get_observation_by_id( +async def get_observation_by_id( session: session_dependency, user: amp_viewer_dependency, observation_id: int ) -> ObservationResponse: return simple_get_by_id(session, Observation, observation_id) @@ -317,7 +317,7 @@ def get_observation_by_id( summary="Delete an observation", status_code=HTTP_204_NO_CONTENT, ) -def delete_observation( +async def delete_observation( session: session_dependency, user: amp_admin_dependency, observation_id: int ) -> None: return model_deleter(session, Observation, observation_id) diff --git a/api/sample.py b/api/sample.py index df701da0f..a16b69d4e 100644 --- a/api/sample.py +++ b/api/sample.py @@ -71,7 +71,7 @@ def database_error_handler( # ============= Post ============================================= @router.post("", status_code=HTTP_201_CREATED) -def add_sample( +async def add_sample( sample_data: CreateSample, session: session_dependency, user: admin_dependency ) -> SampleResponse: """ @@ -85,7 +85,7 @@ def add_sample( # ============= Update ============================================= @router.patch("/{sample_id}", summary="Update Sample") -def update_sample( +async def update_sample( sample_id: int, sample_data: UpdateSample, session: session_dependency, @@ -115,7 +115,7 @@ def update_sample( # ============= Get ============================================= @router.get("", summary="Get Samples") -def get_samples( +async def get_samples( session: session_dependency, user: viewer_dependency, sort: str = None, @@ -132,7 +132,7 @@ def get_samples( @router.get("/{sample_id}", summary="Get Sample by ID") -def get_sample_by_id( +async def get_sample_by_id( sample_id: int, session: session_dependency, user: viewer_dependency ) -> SampleResponse | ResourceNotFoundResponse: """ @@ -145,7 +145,7 @@ def get_sample_by_id( @router.delete("/{sample_id}", summary="Delete Sample by ID") -def delete_sample_by_id( +async def delete_sample_by_id( sample_id: int, session: session_dependency, user: admin_dependency ) -> Response: return model_deleter(session, Sample, sample_id) diff --git a/api/search.py b/api/search.py index fb5be7632..db1d9b661 100644 --- a/api/search.py +++ b/api/search.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from fastapi import APIRouter, Depends +from fastapi import APIRouter from sqlalchemy import select from sqlalchemy.orm import Session from api.pagination import CustomPage @@ -157,7 +157,7 @@ def _get_asset_results(session: Session, q: str, limit: int) -> list[dict]: @router.get("") -def search_api( +async def search_api( session: session_dependency, q: str, limit: int = 25, diff --git a/api/sensor.py b/api/sensor.py index a44aab742..33dbe5c97 100644 --- a/api/sensor.py +++ b/api/sensor.py @@ -38,7 +38,7 @@ @router.post("", status_code=status.HTTP_201_CREATED) -def add_sensor( +async def add_sensor( sensor_data: CreateSensor, session: session_dependency, user: admin_dependency ) -> SensorResponse: """ @@ -51,7 +51,7 @@ def add_sensor( @router.patch("/{sensor_id}", status_code=status.HTTP_200_OK) -def update_sensor( +async def update_sensor( sensor_id: int, sensor_data: UpdateSensor, session: session_dependency, @@ -111,7 +111,7 @@ def update_sensor( @router.delete("/{sensor_id}") -def delete_sensor( +async def delete_sensor( sensor_id: int, session: session_dependency, user: admin_dependency ) -> Response: """ @@ -124,7 +124,7 @@ def delete_sensor( @router.get("", status_code=status.HTTP_200_OK) -def get_sensors( +async def get_sensors( session: session_dependency, user: viewer_dependency, thing_id: int = None, # Optional filter for thing_id @@ -161,7 +161,7 @@ def get_sensors( @router.get("/{sensor_id}", status_code=status.HTTP_200_OK) -def get_sensor( +async def get_sensor( sensor_id: int, session: session_dependency, user: viewer_dependency ) -> SensorResponse: """ diff --git a/api/thing.py b/api/thing.py index f85684625..19a0b94f8 100644 --- a/api/thing.py +++ b/api/thing.py @@ -256,7 +256,7 @@ async def get_spring_by_id( "/id-link", summary="Get all thing links", ) -def get_thing_id_links( +async def get_thing_id_links( session: session_dependency, filter_: str = Query(alias="filter", default=None), sort: str = None, @@ -272,7 +272,7 @@ def get_thing_id_links( @router.get("/id-link/{link_id}", summary="Get thing links by link ID") -def get_thing_id_links( +async def get_thing_id_links( link_id: int, session: session_dependency, ) -> ThingIdLinkResponse: @@ -283,7 +283,7 @@ def get_thing_id_links( @router.get("", summary="Get all things", status_code=HTTP_200_OK) -def get_things( +async def get_things( session: session_dependency, # thing_id: int = None, within: str = None, @@ -322,7 +322,7 @@ async def get_thing_by_id( @router.get("/{thing_id}/id-link", summary="Get thing links by thing ID") -def get_thing_id_links( +async def get_thing_id_links( thing_id: int, session: session_dependency, ) -> CustomPage[ThingIdLinkResponse]: @@ -340,7 +340,7 @@ def get_thing_id_links( @router.post( "/id-link", status_code=HTTP_201_CREATED, summary="Create a new thing link" ) -def create_thing_id_link( +async def create_thing_id_link( link_data: CreateThingIdLink, session: session_dependency, user: admin_dependency, @@ -359,7 +359,7 @@ def create_thing_id_link( summary="Create a water well", status_code=HTTP_201_CREATED, ) -def create_well( +async def create_well( thing_data: CreateWell, session: session_dependency, request: Request, @@ -379,7 +379,7 @@ def create_well( summary="Create a new spring", status_code=HTTP_201_CREATED, ) -def create_spring( +async def create_spring( thing_data: CreateSpring, session: session_dependency, request: Request, @@ -399,7 +399,7 @@ def create_spring( summary="Create a new well screen", status_code=HTTP_201_CREATED, ) -def create_wellscreen( +async def create_wellscreen( session: session_dependency, user: amp_admin_dependency, well_screen_data: CreateWellScreen,