From d505856ea9cb2d0a0a2d34728415ccf87a3b905a Mon Sep 17 00:00:00 2001 From: jakeross Date: Thu, 6 Nov 2025 13:13:57 -0700 Subject: [PATCH 01/16] feat: add transducer observation handling and improve database initialization --- core/initializers.py | 10 ++++------ services/observation_helper.py | 3 +++ tests/features/environment.py | 30 +++++++++++++++++++----------- tests/features/steps/transducer.py | 2 +- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/initializers.py b/core/initializers.py index 3da41018b..e076bd9d7 100644 --- a/core/initializers.py +++ b/core/initializers.py @@ -94,12 +94,10 @@ def init_parameter(path: str = None) -> None: def erase_and_rebuild_db(session: Session): from sqlalchemy import text - with session.bind.connect() as conn: - conn.execute(text("DROP SCHEMA public CASCADE")) - conn.execute(text("CREATE SCHEMA public")) - conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) - conn.commit() - + session.execute(text("DROP SCHEMA public CASCADE")) + session.execute(text("CREATE SCHEMA public")) + session.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) + session.commit() Base.metadata.drop_all(session.bind) Base.metadata.create_all(session.bind) diff --git a/services/observation_helper.py b/services/observation_helper.py index ac5877381..dad284a42 100644 --- a/services/observation_helper.py +++ b/services/observation_helper.py @@ -52,6 +52,9 @@ def get_transducer_observations( order: str | None = None, filter_: str = Query(alias="filter", default=None), ): + if thing_id: + simple_get_by_id(session, Thing, thing_id) + # Subquery to get latest block for each observation block_subq = ( select(TransducerObservationBlock.id) diff --git a/tests/features/environment.py b/tests/features/environment.py index ac97355c1..5707eafea 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -187,9 +187,21 @@ def add_block(context, session, parameter): return block +@add_context_object_container("transducer_observations") +def add_transducer_observation(context, session, block, deployment_id, value): + obs = TransducerObservation( + parameter_id=block.parameter_id, + deployment_id=deployment_id, + observation_datetime=datetime.now(), + value=value, + ) + session.add(obs) + context.objects["transducer_observations"].append(obs) + return obs + + def before_all(context): context.objects = {} - force = False with session_ctx() as session: if session.query(LexiconTerm).count() == 0 or force: @@ -211,16 +223,12 @@ def before_all(context): # parameter ID can be hardcoded because init_parameter always creates the same one 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) + block = add_block(context, session, parameter) + for i in range(1, 10): + add_transducer_observation( + context, session, block, deployment.id, random.random() + ) + session.commit() diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 4b84834ed..2a26b6573 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -52,7 +52,7 @@ def step_impl(context): def step_impl(context): data = context.response.json() context.data = data["items"] - assert len(context.data) > 0 + assert len(context.data) > 0, context.data @then("each transducer data entry should include a timestamp, value, status") From 679f57c9b8c71445935665f4c35ced80c42741ee Mon Sep 17 00:00:00 2001 From: jakeross Date: Thu, 6 Nov 2025 14:02:23 -0700 Subject: [PATCH 02/16] fix: improve assertion messages for transducer data validation --- tests/features/steps/transducer.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 2a26b6573..9af8248e0 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -30,7 +30,7 @@ def step_impl(context): sql = select(TransducerObservation) transducer_observations = session.execute(sql).scalars().all() context.transducer_observations = transducer_observations - assert len(transducer_observations) > 0 + assert len(transducer_observations) > 0, "No transducer observations found" @when("the user requests transducer data for a non-existing well") @@ -52,7 +52,7 @@ def step_impl(context): def step_impl(context): data = context.response.json() context.data = data["items"] - assert len(context.data) > 0, context.data + assert len(context.data) > 0, "Expected at least one transducer data entry" @then("each transducer data entry should include a timestamp, value, status") @@ -60,9 +60,9 @@ def step_impl(context): item = context.data[0]["observation"] block = context.data[0]["block"] - assert "observation_datetime" in item - assert "value" in item - assert "review_status" in block + assert "observation_datetime" in item, f"Expected a timestamp in the data {item}" + assert "value" in item, f"Expected a value in the data {item}" + assert "review_status" in block, f"Expected a review_status in the block {block}" context.timestamp = item["observation_datetime"] context.value = item["value"] @@ -75,7 +75,9 @@ def step_impl(context): from datetime import datetime dt = datetime.fromisoformat(context.timestamp) - assert isinstance(dt, datetime) + assert isinstance( + dt, datetime + ), f"Timestamp is not in ISO 8601 format: {context.timestamp}" @then("the value should be a numeric type") @@ -85,7 +87,7 @@ def step_impl(context): @then('the status should be one of "Draft", "Corrected"') def step_impl(context): - assert context.status in ("not reviewed",) + assert context.status in ("not reviewed",), f"Unexpected status: {context.status}" # ============= EOF ============================================= From 0cdc9e4de49435f414d85d4bd367415655d6ffd3 Mon Sep 17 00:00:00 2001 From: Jake Ross Date: Thu, 6 Nov 2025 15:56:25 -0700 Subject: [PATCH 03/16] Update transducer.py --- tests/features/steps/transducer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 9af8248e0..4cc374166 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -87,7 +87,7 @@ def step_impl(context): @then('the status should be one of "Draft", "Corrected"') def step_impl(context): - assert context.status in ("not reviewed",), f"Unexpected status: {context.status}" + assert context.status in ("Draft", "Corrected"), f'Unexpected status: {context.status} not in "Draft", "Corrected"' # ============= EOF ============================================= From 27fb33843279cfa41b0e521018ea5a1530ddc93f Mon Sep 17 00:00:00 2001 From: jirhiker Date: Thu, 6 Nov 2025 22:56:40 +0000 Subject: [PATCH 04/16] Formatting changes --- tests/features/steps/transducer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 4cc374166..8061c85db 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -87,7 +87,10 @@ def step_impl(context): @then('the status should be one of "Draft", "Corrected"') def step_impl(context): - assert context.status in ("Draft", "Corrected"), f'Unexpected status: {context.status} not in "Draft", "Corrected"' + assert context.status in ( + "Draft", + "Corrected", + ), f'Unexpected status: {context.status} not in "Draft", "Corrected"' # ============= EOF ============================================= From 00ad18bf721eb86c59e0d0431b273b20b7273fa6 Mon Sep 17 00:00:00 2001 From: jross Date: Thu, 6 Nov 2025 16:18:13 -0700 Subject: [PATCH 05/16] fix: enhance data validation and improve well observation retrieval in transducer steps --- tests/features/steps/transducer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 8061c85db..d9f58816d 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -24,13 +24,12 @@ def step_impl(context): with session_ctx() as session: sql = select(Thing).where(Thing.thing_type == "water well") - well = session.execute(sql).scalars().first() - context.well = well + wells = session.execute(sql).scalars().all() + assert len(wells)> 0, "No wells found in db" sql = select(TransducerObservation) transducer_observations = session.execute(sql).scalars().all() - context.transducer_observations = transducer_observations - assert len(transducer_observations) > 0, "No transducer observations found" + assert len(transducer_observations) > 0, "No transducer observations found db" @when("the user requests transducer data for a non-existing well") @@ -51,14 +50,15 @@ def step_impl(context): @then("each page should be an array of transducer data") def step_impl(context): data = context.response.json() - context.data = data["items"] - assert len(context.data) > 0, "Expected at least one transducer data entry" + assert len(data['items']) > 0, "Expected at least one transducer data entry" @then("each transducer data entry should include a timestamp, value, status") def step_impl(context): - item = context.data[0]["observation"] - block = context.data[0]["block"] + data = context.response.json() + items = data[0] + item = items["observation"] + block = items["block"] assert "observation_datetime" in item, f"Expected a timestamp in the data {item}" assert "value" in item, f"Expected a value in the data {item}" From 52c27bfeba7bb0e9454d573887074aec190e60d0 Mon Sep 17 00:00:00 2001 From: jirhiker Date: Thu, 6 Nov 2025 23:18:38 +0000 Subject: [PATCH 06/16] Formatting changes --- tests/features/steps/transducer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index d9f58816d..a4334f6f3 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -25,7 +25,7 @@ def step_impl(context): with session_ctx() as session: sql = select(Thing).where(Thing.thing_type == "water well") wells = session.execute(sql).scalars().all() - assert len(wells)> 0, "No wells found in db" + assert len(wells) > 0, "No wells found in db" sql = select(TransducerObservation) transducer_observations = session.execute(sql).scalars().all() @@ -50,7 +50,7 @@ def step_impl(context): @then("each page should be an array of transducer data") def step_impl(context): data = context.response.json() - assert len(data['items']) > 0, "Expected at least one transducer data entry" + assert len(data["items"]) > 0, "Expected at least one transducer data entry" @then("each transducer data entry should include a timestamp, value, status") From 2e6eb67e9374b11a644a82d45c79382026da2902 Mon Sep 17 00:00:00 2001 From: jross Date: Fri, 7 Nov 2025 15:13:52 -0700 Subject: [PATCH 07/16] fix: replace init_db with erase_and_rebuild_db and update transducer data retrieval logic --- core/app.py | 9 +++++-- core/initializers.py | 42 +++--------------------------- tests/features/environment.py | 6 ++--- tests/features/steps/transducer.py | 6 ++--- 4 files changed, 17 insertions(+), 46 deletions(-) diff --git a/core/app.py b/core/app.py index c5b6bb226..377734e20 100644 --- a/core/app.py +++ b/core/app.py @@ -24,7 +24,12 @@ ) from fastapi.openapi.utils import get_openapi -from .initializers import init_db, init_lexicon, init_parameter, register_routes +from .initializers import ( + init_lexicon, + init_parameter, + register_routes, + erase_and_rebuild_db, +) from .settings import settings @@ -34,7 +39,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: Application lifespan event handler to initialize the database and lexicon. """ if settings.get_enum("MODE") == "development": - init_db() + erase_and_rebuild_db() init_lexicon() init_parameter() diff --git a/core/initializers.py b/core/initializers.py index e076bd9d7..6b0d7920c 100644 --- a/core/initializers.py +++ b/core/initializers.py @@ -21,46 +21,11 @@ from sqlalchemy.orm import Session from db import Base -from db.engine import engine, session_ctx +from db.engine import session_ctx from db.parameter import Parameter from services.lexicon_helper import add_lexicon_term, add_lexicon_category -# ============= EOF ============================================= -def init_db(): - """ - Initialize the database by creating all tables. - This function is called during application startup. - """ - - from sqlalchemy import text - - with engine.connect() as conn: - conn.execute(text("DROP SCHEMA public CASCADE")) - conn.execute(text("CREATE SCHEMA public")) - conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) - conn.commit() - - Base.metadata.drop_all(engine) - Base.metadata.create_all(engine) - - -def init_hypertables(): - """ - Initialize hypertables for time-series data. - This function is called during application startup. - """ - # session = next(get_db_session()) - # Create hypertables for time-series data - with session_ctx() as session: - session.execute( - text("select create_hypertable('observation', 'observation_datetime');") - ) - - # session.commit() - # session.close() - - def init_parameter(path: str = None) -> None: """ Populate the parameter table to allow their use in creating and editing @@ -92,8 +57,6 @@ def init_parameter(path: str = None) -> None: def erase_and_rebuild_db(session: Session): - from sqlalchemy import text - session.execute(text("DROP SCHEMA public CASCADE")) session.execute(text("CREATE SCHEMA public")) session.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) @@ -172,3 +135,6 @@ def register_routes(app): app.include_router(search_router) app.include_router(thing_router) add_pagination(app) + + +# ============= EOF ============================================= diff --git a/tests/features/environment.py b/tests/features/environment.py index 5707eafea..aad9b0dc2 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -24,12 +24,12 @@ Group, GroupThingAssociation, Sensor, - LexiconTerm, TransducerObservation, Parameter, Deployment, TransducerObservationBlock, ) + from db.engine import session_ctx @@ -202,9 +202,9 @@ def add_transducer_observation(context, session, block, deployment_id, value): def before_all(context): context.objects = {} - force = False + rebuild = False with session_ctx() as session: - if session.query(LexiconTerm).count() == 0 or force: + if rebuild: erase_and_rebuild_db(session) init_lexicon() init_parameter() diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index a4334f6f3..2381fb757 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -24,7 +24,7 @@ def step_impl(context): with session_ctx() as session: sql = select(Thing).where(Thing.thing_type == "water well") - wells = session.execute(sql).scalars().all() + wells = session.execute(sql).unique().scalars().all() assert len(wells) > 0, "No wells found in db" sql = select(TransducerObservation) @@ -43,7 +43,7 @@ def step_impl(context): def step_impl(context): context.response = context.client.get( "/observation/transducer-groundwater-level", - params={"thing_id": context.well.id}, + params={"thing_id": context.objects["wells"][0].id}, ) @@ -56,7 +56,7 @@ def step_impl(context): @then("each transducer data entry should include a timestamp, value, status") def step_impl(context): data = context.response.json() - items = data[0] + items = data["items"][0] item = items["observation"] block = items["block"] From 35c764983e126558b863227a710c6ab79ed4b4da Mon Sep 17 00:00:00 2001 From: jross Date: Fri, 7 Nov 2025 16:21:50 -0700 Subject: [PATCH 08/16] fix: update BDD test workflow to include production tag --- .github/workflows/tests.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b71716d6e..d1dd83042 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,22 +66,22 @@ jobs: run: uv run pytest -vv --durations=20 --cov --cov-report=xml --junitxml=junit.xml -# - name: Checkout BDD repo (features only) -# uses: actions/checkout@v4 -# with: -# repository: DataIntegrationGroup/OcotilloBDD -# path: bdd -# -# - name: Copy BDD features into backend test directory -# run: | -# mkdir -p tests/features -# cp -r bdd/features/backend/* tests/features/ -# -# - name: Run BDD tests -# env: -# BASE_URL: ${{ secrets.BACKEND_URL }} -# run: | -# uv run behave tests/features --tags=@backend,@approved --no-capture + - name: Checkout BDD repo (features only) + uses: actions/checkout@v4 + with: + repository: DataIntegrationGroup/OcotilloBDD + path: bdd + + - name: Copy BDD features into backend test directory + run: | + mkdir -p tests/features + cp -r bdd/features/backend/* tests/features/ + + - name: Run BDD tests + env: + BASE_URL: http://localhost:8000 + run: | + uv run behave tests/features --tags=@backend,@production --no-capture - name: Upload results to Codecov uses: codecov/codecov-action@v4 From 8648d015b56cc9ce763ac3a4310555c9193b2690 Mon Sep 17 00:00:00 2001 From: jross Date: Fri, 7 Nov 2025 16:36:32 -0700 Subject: [PATCH 09/16] fix: update BDD test workflow to use combined tags for backend and production --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d1dd83042..acf7b7a45 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,7 +81,7 @@ jobs: env: BASE_URL: http://localhost:8000 run: | - uv run behave tests/features --tags=@backend,@production --no-capture + uv run behave tests/features --tags="@backend and @production" --no-capture - name: Upload results to Codecov uses: codecov/codecov-action@v4 From b0863cd9e8749bc04b3849a4be7ffa851d0bac6e Mon Sep 17 00:00:00 2001 From: jross Date: Fri, 7 Nov 2025 16:59:33 -0700 Subject: [PATCH 10/16] fix: add PostgreSQL environment variables for BDD test execution --- .github/workflows/tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index acf7b7a45..d8e8ed6c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,6 +79,11 @@ jobs: - name: Run BDD tests env: + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + DB_DRIVER: postgres BASE_URL: http://localhost:8000 run: | uv run behave tests/features --tags="@backend and @production" --no-capture From b9bb73e0ae5ce75c25a62eb46ea2b56216fceff4 Mon Sep 17 00:00:00 2001 From: jakeross Date: Thu, 6 Nov 2025 21:34:37 -0700 Subject: [PATCH 11/16] feat: add endpoint for retrieving transducer groundwater level observations and improve query handling --- api/observation.py | 26 +++++++++++++++++++++++++- tests/features/steps/transducer.py | 5 ++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/api/observation.py b/api/observation.py index 3372a2349..576884662 100644 --- a/api/observation.py +++ b/api/observation.py @@ -25,7 +25,7 @@ amp_editor_dependency, amp_viewer_dependency, ) -from db import Observation +from db import Observation, Parameter from schemas.observation import ( CreateGroundwaterLevelObservation, GroundwaterLevelObservationResponse, @@ -113,6 +113,30 @@ async def update_water_chemistry_observation( # ============= Get ============================================== +@router.get( + "/transducer-groundwater-level/{thing_id}", + summary="Get transducer groundwater level observations", +) +async def get_transducer_groundwater_level_observations( + request: Request, + session: session_dependency, + user: amp_viewer_dependency, + thing_id: int, + # parameter_id: int | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, +) -> CustomPage[TransducerObservationWithBlockResponse]: + + groundwater_parameter_id = ( + session.query(Parameter) + .filter(Parameter.parameter_name == "groundwater level") + .one() + .id + ) + + return get_transducer_observations( + session, thing_id, groundwater_parameter_id, start_time, end_time + ) @router.get( diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 2381fb757..552d4ebf6 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -35,15 +35,14 @@ def step_impl(context): @when("the user requests transducer data for a non-existing well") def step_impl(context): context.response = context.client.get( - "/observation/transducer-groundwater-level", params={"thing_id": 9999} + "/observation/transducer-groundwater-level/9999" ) @when("the user requests transducer data for a well") def step_impl(context): context.response = context.client.get( - "/observation/transducer-groundwater-level", - params={"thing_id": context.objects["wells"][0].id}, + f"/observation/transducer-groundwater-level/{context.objects['wells'][0].id}", ) From c0fa00123336d8436acd0f44e83a0f7c07277812 Mon Sep 17 00:00:00 2001 From: jakeross Date: Fri, 7 Nov 2025 23:17:45 -0700 Subject: [PATCH 12/16] fix: remove deprecated transducer groundwater level endpoint and update related tests --- .github/workflows/tests.yml | 7 ++++++- api/observation.py | 19 ------------------- tests/features/steps/transducer.py | 8 ++++---- tests/test_observation.py | 1 + 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d8e8ed6c4..317b5feb1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,7 +86,12 @@ jobs: DB_DRIVER: postgres BASE_URL: http://localhost:8000 run: | - uv run behave tests/features --tags="@backend and @production" --no-capture +# use this when we have consensus on tag nomenclature +# uv run behave tests/features --tags="@backend and @production" --no-capture +# + uv run behave tests/features/transducer-data-response.feature \ + tests/features/thing-type-path-parameters.feature \ + tests/features/thing-query-parameters.feature - name: Upload results to Codecov uses: codecov/codecov-action@v4 diff --git a/api/observation.py b/api/observation.py index 576884662..4917d0a7f 100644 --- a/api/observation.py +++ b/api/observation.py @@ -122,7 +122,6 @@ async def get_transducer_groundwater_level_observations( session: session_dependency, user: amp_viewer_dependency, thing_id: int, - # parameter_id: int | None = None, start_time: datetime | None = None, end_time: datetime | None = None, ) -> CustomPage[TransducerObservationWithBlockResponse]: @@ -139,24 +138,6 @@ async def get_transducer_groundwater_level_observations( ) -@router.get( - "/transducer-groundwater-level", - summary="Get transducer groundwater level observations", -) -async def get_transducer_groundwater_level_observations( - request: Request, - session: session_dependency, - user: amp_viewer_dependency, - thing_id: int | None = None, - parameter_id: int | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, -) -> CustomPage[TransducerObservationWithBlockResponse]: - return get_transducer_observations( - session, thing_id, parameter_id, start_time, end_time - ) - - @router.get("/groundwater-level", summary="Get groundwater level observations") async def get_groundwater_level_observations( request: Request, diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index 552d4ebf6..a6602c821 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -84,12 +84,12 @@ def step_impl(context): assert isinstance(context.value, (int, float)) -@then('the status should be one of "Draft", "Corrected"') +@then('the status should be one of "approved", "not reviewed"') def step_impl(context): assert context.status in ( - "Draft", - "Corrected", - ), f'Unexpected status: {context.status} not in "Draft", "Corrected"' + "approved", + "not reviewed", + ), f'Unexpected status: {context.status} not in "approved", "not reviewed"' # ============= EOF ============================================= diff --git a/tests/test_observation.py b/tests/test_observation.py index 50307b5ca..3a9c7cf10 100644 --- a/tests/test_observation.py +++ b/tests/test_observation.py @@ -211,6 +211,7 @@ def test_patch_water_chemistry_observation_404_wrong_activity_type( # ============= Get tests ================= +@pytest.mark.skip(reason="No longer supported") def test_get_transducer_observations(): response = client.get("/observation/transducer-groundwater-level") assert response.status_code == 200 From 6229d22904788f08e5a3821f6abf5f4f2ebb7ac7 Mon Sep 17 00:00:00 2001 From: jakeross Date: Mon, 10 Nov 2025 08:24:55 -0700 Subject: [PATCH 13/16] fix: remove commented-out SpatiaLite installation and related environment variables from tests.yml --- .github/workflows/tests.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 317b5feb1..4ed80a920 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,11 +36,6 @@ jobs: - name: Check out source repository uses: actions/checkout@v4 -# - name: Install SpatiaLite -# run: | -# sudo apt-get update -# sudo apt-get install -y libsqlite3-mod-spatialite libspatialite-dev - - name: Install uv uses: astral-sh/setup-uv@v5 with: @@ -62,7 +57,6 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres DB_DRIVER: postgres -# SPATIALITE_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu/mod_spatialite.so run: uv run pytest -vv --durations=20 --cov --cov-report=xml --junitxml=junit.xml @@ -88,7 +82,6 @@ jobs: run: | # use this when we have consensus on tag nomenclature # uv run behave tests/features --tags="@backend and @production" --no-capture -# uv run behave tests/features/transducer-data-response.feature \ tests/features/thing-type-path-parameters.feature \ tests/features/thing-query-parameters.feature From 39e7b60c5b17cc8f4283b2b8ad5205a38e987558 Mon Sep 17 00:00:00 2001 From: jakeross Date: Mon, 10 Nov 2025 09:47:21 -0700 Subject: [PATCH 14/16] fix: remove commented-out PostGIS images from tests.yml --- .github/workflows/tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ed80a920..436308636 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Tests +name: Test Suite on: pull_request: @@ -16,11 +16,8 @@ jobs: services: postgis: -# image: ghcr.io/dataintegrationgroup/nmdms:latest -# image: postgres image: postgis/postgis:latest # image: postgis/postgis:17-3.5 -# image: timescale/timescaledb:2.18.0-pg17 env: POSTGRES_PASSWORD: postgres options: >- From f11e2cb227e25d47c9244a56fcc40a5e6bcba3fb Mon Sep 17 00:00:00 2001 From: jakeross Date: Mon, 10 Nov 2025 09:52:50 -0700 Subject: [PATCH 15/16] fix: uncomment PostGIS image version and clean up test run commands in tests.yml --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 436308636..5098607b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: services: postgis: image: postgis/postgis:latest -# image: postgis/postgis:17-3.5 + # image: postgis/postgis:17-3.5 env: POSTGRES_PASSWORD: postgres options: >- @@ -77,11 +77,11 @@ jobs: DB_DRIVER: postgres BASE_URL: http://localhost:8000 run: | -# use this when we have consensus on tag nomenclature -# uv run behave tests/features --tags="@backend and @production" --no-capture - uv run behave tests/features/transducer-data-response.feature \ + uv run behave tests/features/transducer-data-response.feature \ tests/features/thing-type-path-parameters.feature \ tests/features/thing-query-parameters.feature +# use this when we have consensus on tag nomenclature +# uv run behave tests/features --tags="@backend and @production" --no-capture - name: Upload results to Codecov uses: codecov/codecov-action@v4 From bf67ab0c253b414a990c2db083b0a7f725a26d98 Mon Sep 17 00:00:00 2001 From: jakeross Date: Thu, 13 Nov 2025 09:49:14 -0700 Subject: [PATCH 16/16] fix: update transducer groundwater level endpoint to accept optional thing_id and add related test assertions --- api/observation.py | 4 ++-- run_bdd.sh | 8 +++++++- services/observation_helper.py | 5 ++++- tests/features/steps/common.py | 8 ++++++++ tests/features/steps/transducer.py | 4 ++-- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/api/observation.py b/api/observation.py index 4917d0a7f..90970e5a9 100644 --- a/api/observation.py +++ b/api/observation.py @@ -114,14 +114,14 @@ async def update_water_chemistry_observation( # ============= Get ============================================== @router.get( - "/transducer-groundwater-level/{thing_id}", + "/transducer-groundwater-level", summary="Get transducer groundwater level observations", ) async def get_transducer_groundwater_level_observations( request: Request, session: session_dependency, user: amp_viewer_dependency, - thing_id: int, + thing_id: int | None = None, start_time: datetime | None = None, end_time: datetime | None = None, ) -> CustomPage[TransducerObservationWithBlockResponse]: diff --git a/run_bdd.sh b/run_bdd.sh index eb1eaa57f..1f30a4432 100755 --- a/run_bdd.sh +++ b/run_bdd.sh @@ -59,7 +59,13 @@ export BASE_URL=${BASE_URL:-http://localhost:8000} #uv run behave tests/features --tags=@backend #uv run behave tests/features/sensor-notes.feature --tags=@backend +uv run behave tests/features/transducer-data-response.feature + +#uv run behave tests/features/transducer-data-response.feature \ +# tests/features/thing-type-path-parameters.feature \ +# tests/features/thing-query-parameters.feature + +#uv run behave tests/features/well-inventory-csv.feature -uv run behave tests/features --tags=@backend --tags=@production echo "✅ BDD test run complete." diff --git a/services/observation_helper.py b/services/observation_helper.py index dad284a42..af24af05f 100644 --- a/services/observation_helper.py +++ b/services/observation_helper.py @@ -53,7 +53,10 @@ def get_transducer_observations( filter_: str = Query(alias="filter", default=None), ): if thing_id: - simple_get_by_id(session, Thing, thing_id) + item = session.get(Thing, thing_id) + if item is None: + empty_query = select(TransducerObservation).where(False) + return paginate(query=empty_query, conn=session) # Subquery to get latest block for each observation block_subq = ( diff --git a/tests/features/steps/common.py b/tests/features/steps/common.py index af44c8095..e724a6016 100644 --- a/tests/features/steps/common.py +++ b/tests/features/steps/common.py @@ -102,4 +102,12 @@ def step_impl(context): ), f"Unexpected response type {context.response.headers['Content-Type']}" +@then("the items should be an empty list") +def step_impl(context): + data = context.response.json() + assert len(data["items"]) == 0, f'Unexpected items {data["items"]}' + assert data["total"] == 0, f'Unexpected total {data["total"]}' + assert data["page"] == 1, f'Unexpected page {data["page"]}' + + # ============= EOF ============================================= diff --git a/tests/features/steps/transducer.py b/tests/features/steps/transducer.py index a6602c821..9030ba029 100644 --- a/tests/features/steps/transducer.py +++ b/tests/features/steps/transducer.py @@ -35,14 +35,14 @@ def step_impl(context): @when("the user requests transducer data for a non-existing well") def step_impl(context): context.response = context.client.get( - "/observation/transducer-groundwater-level/9999" + "/observation/transducer-groundwater-level?thing_id=9999" ) @when("the user requests transducer data for a well") def step_impl(context): context.response = context.client.get( - f"/observation/transducer-groundwater-level/{context.objects['wells'][0].id}", + f"/observation/transducer-groundwater-level?thing_id={context.objects['wells'][0].id}", )