Skip to content

Commit ee350ea

Browse files
committed
chore: update pydantic and pydantic-core versions, enhance phone number validation, and add CSV feature tests
1 parent 35b15e4 commit ee350ea

13 files changed

Lines changed: 647 additions & 76 deletions

cli/cli.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16+
from collections import defaultdict
1617
from enum import Enum
1718
from pathlib import Path
1819

@@ -70,7 +71,92 @@ def well_inventory_csv(
7071
# TODO: use the same helper function used by api to parse and upload a WI csv
7172
from cli.service_adapter import well_inventory_csv
7273

73-
well_inventory_csv(file_path)
74+
result = well_inventory_csv(file_path)
75+
payload = result.payload if isinstance(result.payload, dict) else {}
76+
summary = payload.get("summary", {})
77+
validation_errors = payload.get("validation_errors", [])
78+
detail = payload.get("detail")
79+
80+
if result.exit_code == 0:
81+
typer.secho("[WELL INVENTORY IMPORT] SUCCESS", fg=typer.colors.GREEN, bold=True)
82+
else:
83+
typer.secho(
84+
"[WELL INVENTORY IMPORT] COMPLETED WITH ISSUES",
85+
fg=typer.colors.BRIGHT_YELLOW,
86+
bold=True,
87+
)
88+
typer.secho("=" * 72, fg=typer.colors.BRIGHT_BLUE)
89+
90+
if summary:
91+
processed = summary.get("total_rows_processed", 0)
92+
imported = summary.get("total_rows_imported", 0)
93+
rows_with_issues = summary.get("validation_errors_or_warnings", 0)
94+
typer.secho("SUMMARY", fg=typer.colors.BRIGHT_BLUE, bold=True)
95+
typer.echo(
96+
f"Summary: processed={processed} imported={imported} rows_with_issues={rows_with_issues}"
97+
)
98+
typer.secho(f" processed : {processed}", fg=typer.colors.CYAN)
99+
typer.secho(f" imported : {imported}", fg=typer.colors.GREEN)
100+
issue_color = (
101+
typer.colors.BRIGHT_YELLOW if rows_with_issues else typer.colors.GREEN
102+
)
103+
typer.secho(f" rows_with_issues : {rows_with_issues}", fg=issue_color)
104+
105+
if validation_errors:
106+
typer.secho("VALIDATION", fg=typer.colors.BRIGHT_BLUE, bold=True)
107+
typer.secho(
108+
f"Validation errors: {len(validation_errors)}",
109+
fg=typer.colors.BRIGHT_YELLOW,
110+
bold=True,
111+
)
112+
grouped_errors = defaultdict(list)
113+
for err in validation_errors:
114+
row = err.get("row", "?")
115+
grouped_errors[row].append(err)
116+
117+
def _row_sort_key(row_value):
118+
try:
119+
return (0, int(row_value))
120+
except (TypeError, ValueError):
121+
return (1, str(row_value))
122+
123+
max_errors_to_show = 100
124+
shown = 0
125+
for row in sorted(grouped_errors.keys(), key=_row_sort_key):
126+
if shown >= max_errors_to_show:
127+
break
128+
129+
row_errors = grouped_errors[row]
130+
typer.secho(
131+
f" Row {row} ({len(row_errors)} issue{'s' if len(row_errors) != 1 else ''})",
132+
fg=typer.colors.CYAN,
133+
bold=True,
134+
)
135+
136+
for err in row_errors:
137+
if shown >= max_errors_to_show:
138+
break
139+
field = err.get("field", "unknown")
140+
message = err.get("error") or err.get("msg") or "validation error"
141+
prefix = typer.style(" ! ", fg=typer.colors.BRIGHT_YELLOW)
142+
field_part = f"\033[1;38;5;208m{field}:\033[0m"
143+
message_part = typer.style(f" {message}", fg=typer.colors.BRIGHT_YELLOW)
144+
typer.echo(f"{prefix}{field_part}{message_part}")
145+
shown += 1
146+
147+
if len(validation_errors) > shown:
148+
typer.secho(
149+
f"... and {len(validation_errors) - shown} more validation errors",
150+
fg=typer.colors.YELLOW,
151+
)
152+
153+
if detail:
154+
typer.secho("ERRORS", fg=typer.colors.BRIGHT_BLUE, bold=True)
155+
typer.secho(f"Error: {detail}", fg=typer.colors.BRIGHT_YELLOW, bold=True)
156+
157+
typer.secho("=" * 72, fg=typer.colors.BRIGHT_BLUE)
158+
159+
raise typer.Exit(result.exit_code)
74160

75161

76162
@water_levels.command("bulk-upload")

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ dependencies = [
6969
"pyasn1==0.6.2",
7070
"pyasn1-modules==0.4.2",
7171
"pycparser==2.23",
72-
"pydantic==2.11.7",
73-
"pydantic-core==2.33.2",
72+
"pydantic==2.12.5",
73+
"pydantic-core==2.41.5",
7474
"pygments==2.19.2",
7575
"pyjwt==2.11.0",
7676
"pyproj==3.7.2",

requirements.txt

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,32 +1092,57 @@ pycparser==2.23 \
10921092
# via
10931093
# cffi
10941094
# ocotilloapi
1095-
pydantic==2.11.7 \
1096-
--hash=sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db \
1097-
--hash=sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b
1095+
pydantic==2.12.5 \
1096+
--hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \
1097+
--hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d
10981098
# via
10991099
# fastapi
11001100
# fastapi-pagination
11011101
# ocotilloapi
1102-
pydantic-core==2.33.2 \
1103-
--hash=sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56 \
1104-
--hash=sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef \
1105-
--hash=sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a \
1106-
--hash=sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f \
1107-
--hash=sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916 \
1108-
--hash=sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a \
1109-
--hash=sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849 \
1110-
--hash=sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e \
1111-
--hash=sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac \
1112-
--hash=sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162 \
1113-
--hash=sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc \
1114-
--hash=sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5 \
1115-
--hash=sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d \
1116-
--hash=sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9 \
1117-
--hash=sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9 \
1118-
--hash=sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5 \
1119-
--hash=sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9 \
1120-
--hash=sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6
1102+
pydantic-core==2.41.5 \
1103+
--hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \
1104+
--hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \
1105+
--hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \
1106+
--hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \
1107+
--hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \
1108+
--hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \
1109+
--hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \
1110+
--hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \
1111+
--hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \
1112+
--hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \
1113+
--hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \
1114+
--hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \
1115+
--hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \
1116+
--hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \
1117+
--hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \
1118+
--hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \
1119+
--hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \
1120+
--hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \
1121+
--hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \
1122+
--hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \
1123+
--hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \
1124+
--hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \
1125+
--hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \
1126+
--hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \
1127+
--hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \
1128+
--hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \
1129+
--hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \
1130+
--hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \
1131+
--hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \
1132+
--hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \
1133+
--hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \
1134+
--hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \
1135+
--hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \
1136+
--hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \
1137+
--hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \
1138+
--hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \
1139+
--hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \
1140+
--hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \
1141+
--hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \
1142+
--hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \
1143+
--hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \
1144+
--hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \
1145+
--hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0
11211146
# via
11221147
# ocotilloapi
11231148
# pydantic

schemas/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from datetime import datetime, timezone, date
1717
from typing import Annotated
1818

19+
from core.enums import ReleaseStatus
1920
from pydantic import (
2021
BaseModel,
2122
ConfigDict,
@@ -26,8 +27,6 @@
2627
from pydantic.json_schema import JsonSchemaValue
2728
from pydantic_core import core_schema
2829

29-
from core.enums import ReleaseStatus
30-
3130
DT_FMT = "%Y-%m-%dT%H:%M:%SZ"
3231

3332

@@ -53,7 +52,12 @@ class BaseUpdateModel(BaseCreateModel):
5352
release_status: ReleaseStatus | None = None
5453

5554

56-
def past_or_today_validator(value: date | datetime) -> date | datetime:
55+
def past_or_today_validator(
56+
value: date | datetime | None,
57+
) -> date | datetime | None:
58+
if value is None:
59+
return None
60+
5761
if isinstance(value, datetime):
5862
if value.tzinfo is None:
5963
if value > datetime.now():

schemas/well_inventory.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@
1919

2020
import phonenumbers
2121
import utm
22-
from pydantic import (
23-
BaseModel,
24-
model_validator,
25-
BeforeValidator,
26-
validate_email,
27-
AfterValidator,
28-
field_validator,
29-
)
30-
3122
from core.constants import STATE_CODES
3223
from core.enums import (
3324
ElevationMethod,
@@ -39,6 +30,15 @@
3930
WellPurpose as WellPurposeEnum,
4031
MonitoringFrequency,
4132
)
33+
from phonenumbers import NumberParseException
34+
from pydantic import (
35+
BaseModel,
36+
model_validator,
37+
BeforeValidator,
38+
validate_email,
39+
AfterValidator,
40+
field_validator,
41+
)
4242
from schemas import past_or_today_validator, PastOrTodayDatetime
4343
from services.util import convert_dt_tz_naive_to_tz_aware
4444

@@ -96,14 +96,18 @@ def phone_validator(phone_number_str):
9696

9797
phone_number_str = phone_number_str.strip()
9898
if phone_number_str:
99-
parsed_number = phonenumbers.parse(phone_number_str, "US")
99+
try:
100+
parsed_number = phonenumbers.parse(phone_number_str, "US")
101+
except NumberParseException as e:
102+
raise ValueError(f"Invalid phone number. {phone_number_str}") from e
103+
100104
if phonenumbers.is_valid_number(parsed_number):
101105
formatted_number = phonenumbers.format_number(
102106
parsed_number, phonenumbers.PhoneNumberFormat.E164
103107
)
104108
return formatted_number
105-
else:
106-
raise ValueError(f"Invalid phone number. {phone_number_str}")
109+
110+
raise ValueError(f"Invalid phone number. {phone_number_str}")
107111

108112

109113
def email_validator_function(email_str):

0 commit comments

Comments
 (0)