From 0ac45c14c80e6f7c9724a581d93c3a4e3911e59e Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 4 Nov 2025 17:31:56 -0700 Subject: [PATCH 1/6] WIP: first stab at well additional information --- .../steps/well-additional-information.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/features/steps/well-additional-information.py diff --git a/tests/features/steps/well-additional-information.py b/tests/features/steps/well-additional-information.py new file mode 100644 index 000000000..89f1c38d9 --- /dev/null +++ b/tests/features/steps/well-additional-information.py @@ -0,0 +1,167 @@ +from behave import when, then + + +@when("the user retrieves the well by ID via path parameter") +def step_impl_retrieve_well_by_id(context): + well_id = 1 + context.response = context.client.get(f"/thing/water-well/{well_id}") + + +# ------------------------------------------------------------------------------ +# Permissions / Operational OK flags +# ------------------------------------------------------------------------------ +# TODO: the API needs to be updated to include Permissions +# TODO: the schema and test data need to be updated +# TODO: should the testing data and tests contain multiple permissions, one that has expired? + + +@then( + "the response should include whether repeat measurement permission is granted for the well" +) +def step_impl(context): + data = context.response.json() + assert data["permissions"][0]["allow_repeat_sampling"] is True + + +@then("the response should include whether sampling permission is granted for the well") +def step_impl(context): + data = context.response.json() + assert data["permissions"][0]["allow_sampling"] is True + + +# TODO: should this be datalogger specific? +@then( + "the response should include whether datalogger installation permission is granted for the well" +) +def step_impl(context): + data = context.response.json() + assert data["permissions"][0]["allow_installation"] is True + + +# ------------------------------------------------------------------------------ +# Well Construction Information +# ------------------------------------------------------------------------------ + + +# TODO: needs to be added to model, schemas, test data +@then("the response should include the completion date of the well") +def step_impl(context): + data = context.response.json() + assert data["completion_date"] == "2020-05-15" + + +# TODO: needs to be added to model, schemas, test data +@then("the response should include the source of the completion information") +def step_impl(context): + data = context.response.json() + assert data["completion_info_source"] == "Driller Report" + + +# TODO: needs to be added to model, schemas, test data +@then("the response should include the driller name") +def step_impl(context): + data = context.response.json() + assert data["driller_name"] == "John Doe" + + +# TODO: needs to be added to model, schemas, test data +# TODO: needs to be an enum and added to lexicon +@then("the response should include the construction method") +def step_impl(context): + data = context.response.json() + assert data["construction_method"] == "Rotary Drilling" + + +# TODO: needs to be added to model, schemas, test data +@then("the response should include the source of the construction information") +def step_impl(context): + data = context.response.json() + assert data["construction_info_source"] == "Driller Report" + + +# ------------------------------------------------------------------------------ +# Additional Well Physical Properties +# ------------------------------------------------------------------------------ + + +# TODO: the transfer script needs to convert ft to in +@then("the response should include the casing diameter in inches") +def step_impl(context): + data = context.response.json() + assert data["casing_diameter"] == 10 + assert data["casing_diameter_unit"] == "in" + + +@then("the response should include the casing depth in feet below ground surface") +def step_impl(context): + data = context.response.json() + assert data["well_casing_depth"] == 30 + assert data["well_casing_depth_unit"] == "ft" + + +# TODO: needs to be added to model, schemas, test data +@then( + "the response should include the casing description (previously casing notes field)" +) +def step_impl(context): + data = context.response.json() + assert data["well_casing_description"] == "test description" + + +# TODO: needs to be added to model, schemas, test data +# TODO: needs to be added to lexicon and an enum should be created +@then("the response should include the well pump type (previously well_type field)") +def step_impl(context): + data = context.response.json() + assert data["well_pump_type"] == "Submersible" + + +# TODO: needs to be added to model, schemas, test data +@then("the response should include the well pump depth in feet (new field)") +def step_impl(context): + data = context.response.json() + assert data["well_pump_depth"] == 100 + assert data["well_pump_depth_unit"] == "ft" + + +# TODO: needs to be added to model, schemas, test data +@then( + "the response should include whether the well is open and suitable for a datalogger" +) +def step_impl(context): + data = context.response.json() + assert data["well_open"] is True + assert data["well_suitable_for_datalogger"] is True + + +# ------------------------------------------------------------------------------ +# Aquifer/ Geology Information +# ------------------------------------------------------------------------------ + + +# TODO: needs to be added to model, schemas, test data +@then( + "the response should include the formation as the formation zone of well completion" +) +def step_impl(context): + data = context.response.json() + assert data["formation"] == "Sandstone" + + +# TODO: needs to be added to model, schemas, test data +@then( + "the response should include the aquifer class code to classify the aquifer into aquifer system." +) +def step_impl(context): + data = context.response.json() + assert data["aquifer_class_code"] == "A1" + + +# TODO: needs to be added to model, schemas, test data +# TODO: should this be plural? that is, a descriptor model of the well +@then( + "the response should include the aquifer type as the type of aquifers penetrated by the well" +) +def step_impl(context): + data = context.response.json() + assert data["aquifer_type"] == "Confined" From edf3c38096f101badc8dfe6abdf64f203daa5604 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Fri, 7 Nov 2025 16:19:08 -0700 Subject: [PATCH 2/6] WIP: well additional information bdd tests --- .../steps/well-additional-information.py | 127 ++++++++++++------ 1 file changed, 89 insertions(+), 38 deletions(-) diff --git a/tests/features/steps/well-additional-information.py b/tests/features/steps/well-additional-information.py index 89f1c38d9..c0f8949ca 100644 --- a/tests/features/steps/well-additional-information.py +++ b/tests/features/steps/well-additional-information.py @@ -3,8 +3,9 @@ @when("the user retrieves the well by ID via path parameter") def step_impl_retrieve_well_by_id(context): - well_id = 1 - context.response = context.client.get(f"/thing/water-well/{well_id}") + context.well = context.objects["wells"][0] + context.response = context.client.get(f"/thing/water-well/{context.well.id}") + context.data = context.response.json() # ------------------------------------------------------------------------------ @@ -13,20 +14,45 @@ def step_impl_retrieve_well_by_id(context): # TODO: the API needs to be updated to include Permissions # TODO: the schema and test data need to be updated # TODO: should the testing data and tests contain multiple permissions, one that has expired? +# TODO: what are the permission_types that will be used? after they have been determined update these tests @then( "the response should include whether repeat measurement permission is granted for the well" ) def step_impl(context): - data = context.response.json() - assert data["permissions"][0]["allow_repeat_sampling"] is True + assert "permissions" in context.data + + permissions = context.well.permissions + water_level_measurement_permissions = [ + p for p in permissions if p.permission_type == "water level measurement" + ] + sorted_water_level_measurement_permissions = sorted( + water_level_measurement_permissions, key=lambda p: p.start_date, reverse=True + ) + + assert ( + context.data["permissions"]["allow_water_level_measurements"] + == sorted_water_level_measurement_permissions[0].permission_allowed + ) @then("the response should include whether sampling permission is granted for the well") def step_impl(context): - data = context.response.json() - assert data["permissions"][0]["allow_sampling"] is True + assert "permissions" in context.data + + permissions = context.well.permissions + water_level_measurement_permissions = [ + p for p in permissions if p.permission_type == "water chemistry sample" + ] + sorted_water_level_measurement_permissions = sorted( + water_level_measurement_permissions, key=lambda p: p.start_date, reverse=True + ) + + assert ( + context.data["permissions"]["allow_sampling"] + == sorted_water_level_measurement_permissions[0].permission_allowed + ) # TODO: should this be datalogger specific? @@ -34,8 +60,20 @@ def step_impl(context): "the response should include whether datalogger installation permission is granted for the well" ) def step_impl(context): - data = context.response.json() - assert data["permissions"][0]["allow_installation"] is True + assert "permissions" in context.data + + permissions = context.well.permissions + water_level_measurement_permissions = [ + p for p in permissions if p.permission_type == "data logger installation" + ] + sorted_water_level_measurement_permissions = sorted( + water_level_measurement_permissions, key=lambda p: p.start_date, reverse=True + ) + + assert ( + context.data["permissions"]["allow_data_logger_installation"] + == sorted_water_level_measurement_permissions[0].permission_allowed + ) # ------------------------------------------------------------------------------ @@ -46,37 +84,42 @@ def step_impl(context): # TODO: needs to be added to model, schemas, test data @then("the response should include the completion date of the well") def step_impl(context): - data = context.response.json() - assert data["completion_date"] == "2020-05-15" + assert "completion_date" in context.data + assert context.data["completion_date"] == context.well.completion_date.strftime( + "%Y-%m-%d" + ) # TODO: needs to be added to model, schemas, test data @then("the response should include the source of the completion information") def step_impl(context): - data = context.response.json() - assert data["completion_info_source"] == "Driller Report" + assert "completion_info_source" in context.data + assert context.data["completion_info_source"] == context.well.completion_info_source # TODO: needs to be added to model, schemas, test data @then("the response should include the driller name") def step_impl(context): - data = context.response.json() - assert data["driller_name"] == "John Doe" + assert "driller_name" in context.data + assert context.data["driller_name"] == context.well.driller_name # TODO: needs to be added to model, schemas, test data # TODO: needs to be an enum and added to lexicon @then("the response should include the construction method") def step_impl(context): - data = context.response.json() - assert data["construction_method"] == "Rotary Drilling" + assert "construction_method" in context.data + assert context.data["construction_method"] == context.well.construction_method # TODO: needs to be added to model, schemas, test data @then("the response should include the source of the construction information") def step_impl(context): - data = context.response.json() - assert data["construction_info_source"] == "Driller Report" + assert "construction_info_source" in context.data + assert ( + context.data["construction_info_source"] + == context.well.construction_info_source + ) # ------------------------------------------------------------------------------ @@ -87,16 +130,20 @@ def step_impl(context): # TODO: the transfer script needs to convert ft to in @then("the response should include the casing diameter in inches") def step_impl(context): - data = context.response.json() - assert data["casing_diameter"] == 10 - assert data["casing_diameter_unit"] == "in" + assert "casing_diameter" in context.data + assert "casing_diameter_unit" in context.data + + assert context.data["casing_diameter"] == context.well.casing_diameter + assert context.data["casing_diameter_unit"] == "in" @then("the response should include the casing depth in feet below ground surface") def step_impl(context): - data = context.response.json() - assert data["well_casing_depth"] == 30 - assert data["well_casing_depth_unit"] == "ft" + assert "well_casing_depth" in context.data + assert "well_casing_depth_unit" in context.data + + assert context.data["well_casing_depth"] == context.well.well_casing_depth + assert context.data["well_casing_depth_unit"] == "ft" # TODO: needs to be added to model, schemas, test data @@ -104,24 +151,28 @@ def step_impl(context): "the response should include the casing description (previously casing notes field)" ) def step_impl(context): - data = context.response.json() - assert data["well_casing_description"] == "test description" + assert "well_casing_description" in context.data + assert ( + context.data["well_casing_description"] == context.well.well_casing_description + ) # TODO: needs to be added to model, schemas, test data # TODO: needs to be added to lexicon and an enum should be created @then("the response should include the well pump type (previously well_type field)") def step_impl(context): - data = context.response.json() - assert data["well_pump_type"] == "Submersible" + assert "well_pump_type" in context.data + assert context.data["well_pump_type"] == context.well.well_pump_type # TODO: needs to be added to model, schemas, test data @then("the response should include the well pump depth in feet (new field)") def step_impl(context): - data = context.response.json() - assert data["well_pump_depth"] == 100 - assert data["well_pump_depth_unit"] == "ft" + assert "well_pump_depth" in context.data + assert "well_pump_depth_unit" in context.data + + assert context.data["well_pump_depth"] == context.well.well_pump_depth + assert context.data["well_pump_depth_unit"] == "ft" # TODO: needs to be added to model, schemas, test data @@ -144,17 +195,17 @@ def step_impl(context): "the response should include the formation as the formation zone of well completion" ) def step_impl(context): - data = context.response.json() - assert data["formation"] == "Sandstone" + assert "formation" in context.data + assert context.data["formation"] == context.well.formation -# TODO: needs to be added to model, schemas, test data +# TODO: needs to be added to model, schemas, test data, lexicon @then( "the response should include the aquifer class code to classify the aquifer into aquifer system." ) def step_impl(context): - data = context.response.json() - assert data["aquifer_class_code"] == "A1" + assert "aquifer_class_code" in context.data + assert context.data["aquifer_class_code"] == context.well.aquifer_class_code # TODO: needs to be added to model, schemas, test data @@ -163,5 +214,5 @@ def step_impl(context): "the response should include the aquifer type as the type of aquifers penetrated by the well" ) def step_impl(context): - data = context.response.json() - assert data["aquifer_type"] == "Confined" + assert "aquifer_type" in context.data + assert context.data["aquifer_type"] == context.well.aquifer_type From 7197edea3d47266e9f54817d52c0c2df72205070 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 10 Nov 2025 09:58:37 -0700 Subject: [PATCH 3/6] feat: use function to retrieve polymoprhic records --- services/util.py | 40 +++++++++++++++++++ .../steps/well-additional-information.py | 32 +++++---------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/services/util.py b/services/util.py index cb3d8826c..36c1bf7a6 100644 --- a/services/util.py +++ b/services/util.py @@ -5,6 +5,7 @@ import httpx from constants import SRID_WGS84 +from db import Base TRANSFORMERS = {} @@ -115,6 +116,45 @@ def get_epqs_elevation_from_point(lon: float, lat: float) -> float | None: return data["value"] +def retrieve_polymorphic_table_record( + target_record: Base, + polymorphic_relationship: str, + polymorphic_type: str, + latest=True, +) -> Base: + """ + Retrieve a record from a polymorphic table. This function assumes that the + parent class has the correct mixin to support retrieval via an attribute. + + Parameters: + ---------- + target_record : Base + The parent record from which to retrieve the polymorphic child record. + + polymorphic_relationship : str + The name of the relationship attribute on the parent record that corresponds to the polymorphic table. + + polymorphic_type : str + The specific type of the polymorphic record to retrieve (e.g., 'Use Status' or 'Monitoring Status' for StatusHistory). + + latest : bool, optional + If True, retrieves the latest record based on start_date. Defaults to True. + """ + if polymorphic_relationship == "permissions": + type_field = "permission_type" + elif polymorphic_relationship == "status_history": + type_field = "status_type" + + polymorphic_records = getattr(target_record, polymorphic_relationship) + type_polymorphic_records = [ + r for r in polymorphic_records if getattr(r, type_field) == polymorphic_type + ] + sorted_type_polymorphic_records = sorted( + type_polymorphic_records, key=lambda r: r.start_date, reverse=latest + ) + return sorted_type_polymorphic_records[0] + + if __name__ == "__main__": x = -106.904107 y = 34.068198 diff --git a/tests/features/steps/well-additional-information.py b/tests/features/steps/well-additional-information.py index c0f8949ca..ead66efaf 100644 --- a/tests/features/steps/well-additional-information.py +++ b/tests/features/steps/well-additional-information.py @@ -1,5 +1,7 @@ from behave import when, then +from services.util import retrieve_polymorphic_table_record + @when("the user retrieves the well by ID via path parameter") def step_impl_retrieve_well_by_id(context): @@ -23,17 +25,13 @@ def step_impl_retrieve_well_by_id(context): def step_impl(context): assert "permissions" in context.data - permissions = context.well.permissions - water_level_measurement_permissions = [ - p for p in permissions if p.permission_type == "water level measurement" - ] - sorted_water_level_measurement_permissions = sorted( - water_level_measurement_permissions, key=lambda p: p.start_date, reverse=True + permission_record = retrieve_polymorphic_table_record( + context.well, "permissions", "allow_water_level_measurements", latest=True ) assert ( context.data["permissions"]["allow_water_level_measurements"] - == sorted_water_level_measurement_permissions[0].permission_allowed + == permission_record.permission_allowed ) @@ -41,17 +39,13 @@ def step_impl(context): def step_impl(context): assert "permissions" in context.data - permissions = context.well.permissions - water_level_measurement_permissions = [ - p for p in permissions if p.permission_type == "water chemistry sample" - ] - sorted_water_level_measurement_permissions = sorted( - water_level_measurement_permissions, key=lambda p: p.start_date, reverse=True + permission_record = retrieve_polymorphic_table_record( + context.well, "permissions", "allow_water_chemistry_sample", latest=True ) assert ( context.data["permissions"]["allow_sampling"] - == sorted_water_level_measurement_permissions[0].permission_allowed + == permission_record.permission_allowed ) @@ -62,17 +56,13 @@ def step_impl(context): def step_impl(context): assert "permissions" in context.data - permissions = context.well.permissions - water_level_measurement_permissions = [ - p for p in permissions if p.permission_type == "data logger installation" - ] - sorted_water_level_measurement_permissions = sorted( - water_level_measurement_permissions, key=lambda p: p.start_date, reverse=True + permission_record = retrieve_polymorphic_table_record( + context.well, "permissions", "allow_data_logger_installation", latest=True ) assert ( context.data["permissions"]["allow_data_logger_installation"] - == sorted_water_level_measurement_permissions[0].permission_allowed + == permission_record.permission_allowed ) From 7d3715404051709ff3b6baadab0252d7fc462358 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 12 Nov 2025 17:33:03 -0700 Subject: [PATCH 4/6] refactor: test for casing materials --- .pre-commit-config.yaml | 18 +++++++++--------- .../steps/well-additional-information.py | 10 +++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d74e6a6c..d708a9010 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,15 +16,15 @@ repos: '--statistics' ] exclude: ^db/__init__.py$ # all models need to be imported for Alembic, but are not used directly - - repo: local - hooks: - - id: pytest - name: pytest - entry: pytest # Or your specific test command, e.g., poetry run pytest - language: system - types: [python] # Specify relevant file types for your tests - pass_filenames: false - always_run: true + # - repo: local + # hooks: + # - id: pytest + # name: pytest + # entry: pytest # Or your specific test command, e.g., poetry run pytest + # language: system + # types: [python] # Specify relevant file types for your tests + # pass_filenames: false + # always_run: true # - repo: https://github.com/pre-commit/mirrors-mypy # rev: v1.10.0 # Use the latest stable version or pin to your preference diff --git a/tests/features/steps/well-additional-information.py b/tests/features/steps/well-additional-information.py index ead66efaf..953f065b0 100644 --- a/tests/features/steps/well-additional-information.py +++ b/tests/features/steps/well-additional-information.py @@ -137,14 +137,10 @@ def step_impl(context): # TODO: needs to be added to model, schemas, test data -@then( - "the response should include the casing description (previously casing notes field)" -) +@then("the response should include the casing materials") def step_impl(context): - assert "well_casing_description" in context.data - assert ( - context.data["well_casing_description"] == context.well.well_casing_description - ) + assert "well_casing_materials" in context.data + assert context.data["well_casing_materials"] == context.well.well_casing_materials # TODO: needs to be added to model, schemas, test data From c0b80fb6e02135c479fa2a2367f439697855a069 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 12 Nov 2025 17:42:42 -0700 Subject: [PATCH 5/6] fix: fix casing materials test --- tests/features/steps/well-additional-information.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/features/steps/well-additional-information.py b/tests/features/steps/well-additional-information.py index 953f065b0..02aa4ff22 100644 --- a/tests/features/steps/well-additional-information.py +++ b/tests/features/steps/well-additional-information.py @@ -140,7 +140,9 @@ def step_impl(context): @then("the response should include the casing materials") def step_impl(context): assert "well_casing_materials" in context.data - assert context.data["well_casing_materials"] == context.well.well_casing_materials + assert sorted(context.data["well_casing_materials"]) == sorted( + [m.material for m in context.well.well_casing_materials] + ) # TODO: needs to be added to model, schemas, test data From 878ef3bb1dade77a6969805267636aeadb849785 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 13 Nov 2025 08:30:33 -0700 Subject: [PATCH 6/6] fix: uncomment pytest from pre commit --- .pre-commit-config.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d708a9010..5d74e6a6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,15 +16,15 @@ repos: '--statistics' ] exclude: ^db/__init__.py$ # all models need to be imported for Alembic, but are not used directly - # - repo: local - # hooks: - # - id: pytest - # name: pytest - # entry: pytest # Or your specific test command, e.g., poetry run pytest - # language: system - # types: [python] # Specify relevant file types for your tests - # pass_filenames: false - # always_run: true + - repo: local + hooks: + - id: pytest + name: pytest + entry: pytest # Or your specific test command, e.g., poetry run pytest + language: system + types: [python] # Specify relevant file types for your tests + pass_filenames: false + always_run: true # - repo: https://github.com/pre-commit/mirrors-mypy # rev: v1.10.0 # Use the latest stable version or pin to your preference