Skip to content

Commit 089efce

Browse files
authored
Merge pull request #466 from DataIntegrationGroup/water-level-csv-refactor
BDMS 412/529/530: water level csv implementation
2 parents 3bb4097 + 077b9da commit 089efce

15 files changed

Lines changed: 1070 additions & 513 deletions

api/observation.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
UpdateWaterChemistryObservation,
4242
)
4343
from schemas.transducer import TransducerObservationWithBlockResponse
44-
from schemas.water_level_csv import WaterLevelBulkUploadResponse
44+
from schemas.water_level_csv import WaterLevelBulkUploadPayload
4545
from services.crud_helper import model_deleter, model_adder
4646
from services.observation_helper import (
4747
get_observations,
@@ -90,8 +90,8 @@ async def add_water_chemistry_observation(
9090

9191
@router.post(
9292
"/groundwater-level/bulk-upload",
93-
response_model=WaterLevelBulkUploadResponse,
94-
status_code=HTTP_200_OK,
93+
response_model=WaterLevelBulkUploadPayload,
94+
status_code=HTTP_201_CREATED,
9595
)
9696
async def bulk_upload_groundwater_levels(
9797
user: amp_admin_dependency,
@@ -107,7 +107,9 @@ async def bulk_upload_groundwater_levels(
107107
result = bulk_upload_water_levels(contents)
108108

109109
if result.exit_code != 0:
110-
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=result.payload)
110+
raise HTTPException(
111+
status_code=HTTP_400_BAD_REQUEST, detail=result.payload.model_dump()
112+
)
111113

112114
return result.payload
113115

cli/cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def water_levels_bulk_upload(file_path: str, output_format: str | None):
8383
"""
8484
parse and upload a csv
8585
"""
86-
# TODO: use the same helper function used by api to parse and upload a WL csv
8786
from cli.service_adapter import water_levels_csv
8887

8988
pretty_json = (output_format or "").lower() == "json"

schemas/water_level_csv.py

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,140 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16-
from pydantic import BaseModel
16+
from datetime import datetime
17+
from pydantic import BaseModel, ConfigDict, field_validator, Field
18+
from typing import Any
1719

20+
from core.enums import SampleMethod, GroundwaterLevelReason, GroundwaterLevelAccuracy
1821

19-
class WaterLevelBulkUploadSummary(BaseModel):
20-
total_rows_processed: int
21-
total_rows_imported: int
22-
validation_errors_or_warnings: int
22+
23+
class WaterLevelCsvRow(BaseModel):
24+
"""
25+
This class defines the schema for a single row in the water level CSV upload.
26+
"""
27+
28+
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
29+
30+
well_name_point_id: str = Field(
31+
description="Name/PointID of the well where the measurement was taken."
32+
)
33+
field_event_date_time: datetime = Field(
34+
description="Date and time when the field event occurred."
35+
)
36+
field_staff: str = Field(description="Name of the person who led the field event.")
37+
field_staff_2: str | None = Field(
38+
description="Name of the second person who participated in the field event.",
39+
default=None,
40+
)
41+
field_staff_3: str | None = Field(
42+
description="Name of the third person who participated in the field event.",
43+
default=None,
44+
)
45+
water_level_date_time: datetime = Field(
46+
description="Date and time when the water level measurement was taken."
47+
)
48+
measuring_person: str = Field(
49+
description="Person who took the water level measurement. They must be one of the field staff"
50+
)
51+
sample_method: SampleMethod = Field(
52+
description="Method used to measure the water level."
53+
)
54+
mp_height: float = Field(
55+
description="Measuring point height relative to the ground surface in feet."
56+
)
57+
level_status: GroundwaterLevelReason = Field(
58+
description="Status of the water level."
59+
)
60+
depth_to_water_ft: float = Field(description="Depth to water in feet.")
61+
data_quality: GroundwaterLevelAccuracy = Field(
62+
description="A description of the accuracy of the data."
63+
)
64+
water_level_notes: str | None = Field(
65+
description="Additional notes about the water level measurement.", default=None
66+
)
67+
68+
@field_validator("water_level_notes", mode="before")
69+
@classmethod
70+
def _empty_to_none(cls, value: str | None) -> str | None:
71+
if value is None:
72+
return None
73+
if isinstance(value, str) and value.strip() == "":
74+
return None
75+
return value
76+
77+
@field_validator("measuring_person")
78+
@classmethod
79+
def ensure_measuring_person_is_field_staff(
80+
cls, value: str, values: dict[str, Any]
81+
) -> str:
82+
data = values.data
83+
field_staffs = [
84+
data.get("field_staff"),
85+
data.get("field_staff_2"),
86+
data.get("field_staff_3"),
87+
]
88+
if value not in field_staffs:
89+
raise ValueError("measuring_person must be one of the field staff")
90+
return value
91+
92+
93+
class WaterLevelBulkUploadRow(WaterLevelCsvRow):
94+
"""
95+
This class extends WaterLevelCsvRow to include resolved database objects
96+
for easier processing during bulk upload.
97+
"""
98+
99+
well: Any = Field(description="The Thing object representing the well.")
100+
field_staff_contact: Any = Field(
101+
description="The Contact object for the field staff."
102+
)
103+
field_staff_2_contact: Any | None = Field(
104+
description="The Contact object for the second field staff."
105+
)
106+
field_staff_3_contact: Any | None = Field(
107+
description="The Contact object for the third field staff."
108+
)
109+
measuring_person_field_staff_index: int = Field(
110+
description="The index of the field staff who is the measuring person: 1, 2, or 3."
111+
)
23112

24113

25-
class WaterLevelBulkUploadRow(BaseModel):
114+
class WaterLevelCreatedRow(BaseModel):
115+
"""
116+
This class defines the structure of a successfully created water level row
117+
during bulk upload.
118+
"""
119+
26120
well_name_point_id: str
27121
field_event_id: int
28122
field_activity_id: int
123+
field_event_participant_1_id: int
124+
field_event_participant_2_id: int | None
125+
field_event_participant_3_id: int | None
29126
sample_id: int
30127
observation_id: int
31-
measurement_date_time: str
32-
level_status: str
33-
data_quality: str
128+
water_level_date_time: str
129+
groundwater_level_reason: str
130+
groundwater_level_accuracy: str
34131

35132

36-
class WaterLevelBulkUploadResponse(BaseModel):
133+
class WaterLevelBulkUploadSummary(BaseModel):
134+
total_rows_processed: int
135+
total_rows_imported: int
136+
total_validation_errors_or_warnings: int
137+
138+
139+
class WaterLevelBulkUploadPayload(BaseModel):
37140
summary: WaterLevelBulkUploadSummary
38-
water_levels: list[WaterLevelBulkUploadRow]
141+
water_levels: list[WaterLevelCreatedRow]
39142
validation_errors: list[str]
40143

41144

145+
class WaterLevelBulkUploadResponse(BaseModel):
146+
exit_code: int
147+
stdout: str
148+
stderr: str
149+
payload: WaterLevelBulkUploadPayload
150+
151+
42152
# ============= EOF =============================================

0 commit comments

Comments
 (0)