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: 2 additions & 0 deletions cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,9 @@ def import_project_area_boundaries_command(
result = import_project_area_boundaries(layer_url=layer_url)
typer.echo(f"Fetched {result.fetched} feature(s).")
typer.echo(f"Matched {result.matched} group row(s).")
typer.echo(f"Created {result.created} group(s).")
typer.echo(f"Updated {result.updated} group project area(s).")
typer.echo(f"Skipped {result.skipped} unchanged group(s).")
Comment on lines 1023 to +1026
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Existing CLI tests for import-project-area-boundaries (see tests/test_cli_commands.py::test_import_project_area_boundaries_updates_matching_groups) assert the old output/behavior (including "Unmatched locations"). With the new Created/Skipped output and the new-group creation path, those tests should be updated and extended to cover the created/skipped counts.

Copilot uses AI. Check for mistakes.
if result.unmatched_locations:
typer.echo(
"Unmatched locations: " + ", ".join(result.unmatched_locations),
Expand Down
50 changes: 42 additions & 8 deletions cli/project_area_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ class ProjectAreaImportResult:
fetched: int
matched: int
updated: int
created: int
skipped: int
unmatched_locations: tuple[str, ...]


def _normalize_name(value: str) -> str:
return value.strip().lower()


def _geoms_equal(geom1: str, geom2: str) -> bool:
from shapely import wkt

return wkt.loads(geom1).equals(wkt.loads(geom2))


def _geojson_to_multipolygon_wkt(geometry: dict[str, Any]) -> str:
geom = shape(geometry)
if isinstance(geom, Polygon):
Expand Down Expand Up @@ -77,13 +85,16 @@ def _fetch_project_area_features(

def import_project_area_boundaries(
layer_url: str = PROJECT_AREA_LAYER_URL,
group_type: str = "Geographic Area",
) -> ProjectAreaImportResult:
with httpx.Client(timeout=60.0) as client:
features = _fetch_project_area_features(client, layer_url)

unmatched_locations: list[str] = []
matched = 0
updated = 0
created = 0
skipped = 0

with session_ctx() as session:
for feature in features:
Expand All @@ -94,30 +105,53 @@ def import_project_area_boundaries(
if not location_name or geometry is None:
continue

normalized_name = _normalize_name(location_name)
groups = session.scalars(
select(Group).where(
func.lower(func.trim(Group.name)) == _normalize_name(location_name)
func.lower(func.trim(Group.name)) == normalized_name,
Group.group_type == group_type,
)
).all()

if not groups:
unmatched_locations.append(location_name)
continue

matched += len(groups)
project_area = WKTElement(
_geojson_to_multipolygon_wkt(geometry),
srid=4326,
)

if not groups:
new_group = Group(
name=location_name,
group_type=group_type,
project_area=project_area,
Comment on lines +121 to +125
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The if not groups: branch now creates a new Group instead of recording an unmatched location, so unmatched_locations is never populated and any downstream "Unmatched locations" reporting becomes dead code. Either remove the unmatched_locations field/output or repurpose it to track skipped/invalid features if you still need operator visibility.

Copilot uses AI. Check for mistakes.
)
session.add(new_group)
created += 1
matched += 1
continue

matched += len(groups)
for group in groups:
group.project_area = project_area
updated += 1
old_wkt = None
if group.project_area is not None:
from shapely import wkb

old_wkt = wkb.loads(bytes(group.project_area.data)).wkt

new_wkt = project_area.desc

if old_wkt is None or not _geoms_equal(old_wkt, new_wkt):
group.project_area = project_area
Comment on lines +140 to +143
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

new_wkt = project_area.desc is unlikely to be a plain WKT string (GeoAlchemy2 elements typically use .desc for SQL compilation), but _geoms_equal() expects WKT input for shapely.wkt.loads(). This can cause parse errors or false comparisons, leading to unnecessary updates or crashes. Use a real WKT string for comparison (e.g., the string returned by _geojson_to_multipolygon_wkt() or project_area.data) and consider computing it once to avoid repeated conversions.

Copilot uses AI. Check for mistakes.
updated += 1
else:
skipped += 1

session.commit()

return ProjectAreaImportResult(
fetched=len(features),
matched=matched,
updated=updated,
created=created,
skipped=skipped,
unmatched_locations=tuple(sorted(set(unmatched_locations))),
)
Loading