fix(site-finder): address PR #90 auto-review minor feedback
1. TS PipelineObject.distance_m — `number | null` для отражения defensive
Python guard (`if obj.get("distance_m") is not None`). Comment объясняет
почему.
2. Pipeline SQL: `text(f"...")` → `text("...")` + parameters. radius_m и
horizon_months через `:param` placeholders + `cast(:horizon_months || ' months'
AS interval)`. Consistency с остальными SQL в файле, plus защита от
accidental injection при будущих изменениях.
3. top_objects: explicit field selection вместо `dict(r) for r in rows`.
Раньше leak'ило все колонки из CTE `SELECT *` (latitude/longitude/
snapshot_date/region_cd/dev_id) в API response. Теперь только nominated
fields: obj_id, comm_name, dev_name, obj_class, flat_count, ready_dt,
distance_m. Schema clean.
Per auto-review on 4e431bf.
This commit is contained in:
parent
4e431bf950
commit
8f804d15dd
2 changed files with 37 additions and 20 deletions
|
|
@ -415,21 +415,31 @@ def _aggregate_pipeline(rows: list[Any]) -> dict[str, Any]:
|
|||
# Sort quarters chronologically
|
||||
quarters_sorted = [{"quarter": k, **v} for k, v in sorted(by_quarter.items())]
|
||||
|
||||
# Top objects — по flat_count desc
|
||||
top_objects = sorted(
|
||||
[dict(r) for r in rows],
|
||||
key=lambda r: r.get("flat_count") or 0,
|
||||
reverse=True,
|
||||
)[:PIPELINE_TOP_OBJECTS_LIMIT]
|
||||
|
||||
# Serialize date for JSON; distance_m — explicit None check, не falsy,
|
||||
# потому что centroid-on-building даёт 0.0 (falsy) и raw Decimal упал бы
|
||||
# в JSON serialization.
|
||||
for obj in top_objects:
|
||||
if obj.get("ready_dt"):
|
||||
obj["ready_dt"] = obj["ready_dt"].isoformat()
|
||||
if obj.get("distance_m") is not None:
|
||||
obj["distance_m"] = round(float(obj["distance_m"]))
|
||||
# Top objects — по flat_count desc.
|
||||
# Explicit field selection вместо `dict(r)` — иначе CTE `SELECT *` протекает
|
||||
# внутренние колонки (latitude/longitude/snapshot_date/region_cd/dev_id и т.д.)
|
||||
# в API response. Не security issue, но schema-leak.
|
||||
top_rows = sorted(rows, key=lambda r: r.get("flat_count") or 0, reverse=True)[
|
||||
:PIPELINE_TOP_OBJECTS_LIMIT
|
||||
]
|
||||
top_objects: list[dict[str, Any]] = []
|
||||
for r in top_rows:
|
||||
ready_dt = r.get("ready_dt")
|
||||
distance_m = r.get("distance_m")
|
||||
top_objects.append(
|
||||
{
|
||||
"obj_id": r["obj_id"],
|
||||
"comm_name": r.get("comm_name"),
|
||||
"dev_name": r.get("dev_name"),
|
||||
"obj_class": r.get("obj_class"),
|
||||
"flat_count": r.get("flat_count"),
|
||||
# ISO date string для JSON; distance_m — explicit None guard
|
||||
# (centroid-on-building даёт 0.0 — falsy float; raw Decimal иначе
|
||||
# упадёт в JSON serialization).
|
||||
"ready_dt": ready_dt.isoformat() if ready_dt else None,
|
||||
"distance_m": round(float(distance_m)) if distance_m is not None else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"objects_count": len(rows),
|
||||
|
|
@ -1159,7 +1169,7 @@ def analyze_parcel(
|
|||
# (latitude, longitude) — отдельный issue для database-expert.
|
||||
pipeline_rows = (
|
||||
db.execute(
|
||||
text(f"""
|
||||
text("""
|
||||
WITH latest_obj AS (
|
||||
SELECT DISTINCT ON (obj_id) *
|
||||
FROM domrf_kn_objects
|
||||
|
|
@ -1181,13 +1191,17 @@ def analyze_parcel(
|
|||
WHERE ST_DWithin(
|
||||
ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography,
|
||||
ST_Centroid(ST_GeomFromText(:wkt, 4326))::geography,
|
||||
{PIPELINE_RADIUS_M}
|
||||
:radius_m
|
||||
)
|
||||
AND ready_dt >= CURRENT_DATE
|
||||
AND ready_dt < CURRENT_DATE + INTERVAL '{PIPELINE_HORIZON_MONTHS} months'
|
||||
AND ready_dt < CURRENT_DATE + cast(:horizon_months || ' months' AS interval)
|
||||
ORDER BY ready_dt ASC
|
||||
"""),
|
||||
{"wkt": geom_wkt},
|
||||
{
|
||||
"wkt": geom_wkt,
|
||||
"radius_m": PIPELINE_RADIUS_M,
|
||||
"horizon_months": str(PIPELINE_HORIZON_MONTHS),
|
||||
},
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
|
|
|
|||
|
|
@ -185,7 +185,10 @@ export interface PipelineObject {
|
|||
obj_class: string | null;
|
||||
flat_count: number | null;
|
||||
ready_dt: string | null;
|
||||
distance_m: number;
|
||||
// backend имеет explicit `is not None` guard для distance_m (centroid-on-
|
||||
// building даёт 0.0 которое falsy, raw Decimal иначе упал бы в JSON), так
|
||||
// что nullable отражает defensive Python path.
|
||||
distance_m: number | null;
|
||||
}
|
||||
|
||||
export interface Pipeline24mo {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue