Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.openapi.utils import get_openapi
from fastapi_pagination import add_pagination

from .initializers import init_db, init_lexicon, init_parameter, register_routes
from .settings import settings
Expand All @@ -40,7 +39,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
init_parameter()

register_routes(app)
add_pagination(app)
yield


Expand Down
2 changes: 2 additions & 0 deletions core/initializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# ===============================================================================
from pathlib import Path

from fastapi_pagination import add_pagination
from sqlalchemy import text
from sqlalchemy.exc import DatabaseError
from sqlalchemy.orm import Session
Expand Down Expand Up @@ -172,3 +173,4 @@ def register_routes(app):
app.include_router(sensor_router)
app.include_router(search_router)
app.include_router(thing_router)
add_pagination(app)
Comment thread
jirhiker marked this conversation as resolved.
5 changes: 5 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os

import sentry_sdk
from dotenv import load_dotenv

from core.initializers import register_routes

load_dotenv()

sentry_sdk.init(
Expand All @@ -26,6 +29,8 @@

from core.app import app

register_routes(app)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins, adjust as needed for security
Expand Down
8 changes: 5 additions & 3 deletions services/observation_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ def get_transducer_observations(
)

if thing_id is not None:
sql = sql.join(Deployment)
sql = sql.join(Thing)
sql = sql.where(Thing.id == thing_id)

thing = simple_get_by_id(session, Thing, thing_id)
if thing:
sql = sql.join(Deployment)
sql = sql.where(Deployment.thing == thing)

if start_time:
sql = sql.where(TransducerObservation.observation_datetime >= start_time)
Expand Down
249 changes: 249 additions & 0 deletions tests/features/environment.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to me to tear down all of the objects after the tests have run. If not, they'll persist and cause issues down the line, as well as hide potential API behavior.

I think it'd be good to uncomment the after_all, or remove the if statement in before_all to ensure that the tests always start with a blank database.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the things that I appreciated about pydantic's fixtures is the ease of calling and identifying them. What're your thoughts on storing them in a dictionary organized by object name and then ID?

e.g.

context.objects = {
    "thing": {
        1: {water well with ID 1}
        2: [water well with ID 2}
    },
    "location": {
        1: {location with ID 1}
       ...
    }
    ...
}

Then when I want to retrieve the thing with ID 1 I could call context.objects["thing"][1] and have access to it (and its attributes). Single table inheritance may make this slightly more complicated as we add other data types, but the complications wouldn't be all that great

@jirhiker jirhiker Nov 5, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that any data added to the db during a step function should be removed, however I don't see the point in removing the seed data. Deleting the seed data and reloading actually has the unintended consequence of changing the id so that objects can no longer simply be referred to by id. e.g. when sensor.id=1 is removed from the db and reloaded it is assigned sensor.id=2

The add_ functions are all idempotent which will address " If not, they'll persist and cause issues down the line, as well as hide potential API behavior."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    "thing": {
        1: {water well with ID 1}
        2: [water well with ID 2}
    },
    "location": {
        1: {location with ID 1}
       ...
    }
    ...
}```
this is fine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If run_bdd.sh is run more than once, the same records will be added to the database n time. This may not cause issues for all models, but there are some that have unique constraints, like sensors, which will cause database errors.

I don't think that we need to know about specific IDs (1, 2, 3, ...), we just need to know that an object is in the database and that we can retrieve and use its ID (like how the pytest fixtures are currently used). Since the records are persisted in a list it won't matter what their IDs are. The test can invoke context.objects["wells"][0].id to get the relevant ID and we won't need to worry about/know its value.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If run_bdd.sh is run more than once, the same records will be added to the database n time[s]" That is not true. all the add_ functions test if the object is in the database before adding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that 🤦, then this should work well 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# ===============================================================================
# Copyright 2025 ross
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# =============== ================================================================
import random
from datetime import datetime, timedelta

from core.initializers import erase_and_rebuild_db, init_lexicon, init_parameter
from db import (
Location,
Thing,
LocationThingAssociation,
Group,
GroupThingAssociation,
Sensor,
LexiconTerm,
TransducerObservation,
Parameter,
Deployment,
TransducerObservationBlock,
)
from db.engine import session_ctx


def add_context_object_container(name):
def wrapper(func):
def closure(context, *args, **kwargs):
if name not in context.objects:
context.objects[name] = []
return func(context, *args, **kwargs)

return closure

return wrapper


@add_context_object_container("locations")
def add_location(context, session, lid):
loc = session.get(Location, lid)

if not loc:
loc = Location(
# name="first location",
notes="these are some test notes",
point="POINT(-107.949533 33.809665)",
elevation=2464.9,
release_status="draft",
elevation_accuracy=100,
elevation_method="Survey-grade GPS",
coordinate_accuracy=50,
coordinate_method="GPS, uncorrected",
)
session.add(loc)
session.commit()
session.refresh(loc)

context.objects["locations"].append(loc)
return loc


@add_context_object_container("wells")
def add_well(context, session, location, thing_id):
well = session.get(Thing, thing_id)
if well is None:
well = Thing(
name=f"WL-{thing_id:04d}",
first_visit_date="2023-03-03",
thing_type="water well",
release_status="draft",
well_depth=10,
hole_depth=10,
well_construction_notes="Test well construction notes",
well_casing_diameter=5.0,
well_casing_depth=10.0,
)
session.add(well)
session.commit()

assoc = LocationThingAssociation(location=location, thing=well)
assoc.effective_start = "2025-02-01T00:00:00Z"
session.add(assoc)
session.commit()

session.refresh(well)

context.objects["wells"].append(well)
return well


@add_context_object_container("springs")
def add_spring(context, session, location, thing_id):
spring = session.get(Thing, thing_id)
if spring is None:
spring = Thing(
name=f"SP-{thing_id:04d}",
first_visit_date="2023-03-03",
thing_type="spring",
release_status="draft",
# well_depth=10,
# hole_depth=10,
# well_construction_notes="Test well construction notes",
# well_casing_diameter=5.0,
# well_casing_depth=10.0,
)
session.add(spring)
session.commit()

assoc = LocationThingAssociation(location=location, thing=spring)
assoc.effective_start = "2025-02-01T00:00:00Z"
session.add(assoc)
session.commit()

session.refresh(spring)
context.objects["springs"].append(spring)


@add_context_object_container("sensors")
def add_sensor(context, session, sid):
sensor = session.get(Sensor, sid)
if sensor is None:
sensor = Sensor(
name="Test Sensor",
sensor_type="Pressure Transducer",
model="Model X",
serial_no="123456",
pcn_number="PCN123456",
owner_agency="NMBGMR",
sensor_status="In Service",
notes="Test equipment",
release_status="draft",
)
session.add(sensor)
session.commit()
session.refresh(sensor)

context.objects["sensors"].append(sensor)
return sensor


@add_context_object_container("groups")
def add_group(context, session, wells, gid):
group = session.get(Group, gid)
if not group:
group = Group(name="Collabnet")
for w in wells:
assoc = GroupThingAssociation(group=group, thing=w)
session.add(assoc)

session.add(group)
session.commit()
session.refresh(group)

context.objects["groups"].append(group)


@add_context_object_container("deployments")
def add_deployment(context, session, sid):
deployment = session.get(Deployment, sid)
if deployment is None:
deployment = Deployment(
thing_id=1,
sensor_id=1,
installation_date=datetime.now(),
)
session.add(deployment)
session.commit()
session.refresh(deployment)

context.objects["deployments"].append(deployment)
return deployment


@add_context_object_container("blocks")
def add_block(context, session, parameter):
block = (
session.query(TransducerObservationBlock)
.filter(TransducerObservationBlock.parameter_id == parameter.id)
.one_or_none()
)
add_obs = False
if block is None:
block = TransducerObservationBlock(
parameter_id=parameter.id,
start_datetime=datetime.now() - timedelta(hours=1),
end_datetime=datetime.now() + timedelta(hours=1),
review_status="not reviewed",
)

session.add(block)
session.commit()
session.refresh(block)
add_obs = True

context.objects["blocks"].append(block)
return add_obs


def before_all(context):
context.objects = {}

force = False
with session_ctx() as session:
if force or session.query(LexiconTerm).count() == 0:
erase_and_rebuild_db(session)
init_lexicon()
init_parameter()

loc = add_location(context, session, 1)
loc2 = add_location(context, session, 2)
loc3 = add_location(context, session, 3)
loc4 = add_location(context, session, 4)

add_well(context, session, loc, 1)
add_well(context, session, loc2, 2)
add_well(context, session, loc3, 3)
add_spring(context, session, loc4, 4)
add_sensor(context, session, 1)
deployment = add_deployment(context, session, 1)

parameter = session.get(Parameter, 1)
add_obs = add_block(context, session, parameter)
if add_obs:
for i in range(1, 10):
obs = TransducerObservation(
parameter_id=parameter.id,
deployment_id=deployment.id,
observation_datetime=datetime.now(),
value=random.random(),
)
session.add(obs)
session.commit()


def after_all(context):
pass


# ============= EOF =============================================
Loading
Loading