security: pip-audit + bandit gates that can fail; patch 29 known CVEs
security.yml previously ran safety/bandit/semgrep with `|| true` and could not go red. Now: pip-audit on requirements.txt is a hard gate (dev deps reported only), bandit HIGH fails (B104 bind-all skipped: intended behind Cloudflare/Caddy), pip-licenses uploaded as a report. Weekly + on dependency/source changes. Running it locally found 29 advisories, all in pinned-and-forgotten runtime deps: starlette 0.27 (7, incl. Host-header path confusion and form DoS), fastapi 0.104, requests 2.31 (3), pymysql 1.1. Bumped to current: fastapi 0.141.1 / starlette 1.6.0, pydantic 2.13.5, uvicorn 0.52.4, requests 2.34.2, pymysql 1.2.0; dev: pytest 9.1.1, black 26.5.1. pip-audit is now clean. requires-python narrowed to 3.11 (the truth: psycopg2-binary 2.9.9 fails on 3.13; pandas 2.0.3 has no 3.12 wheels). Full suite passes; API smoke-tested (health, stations, forecast, history, stats, docs, openapi) on the new stack. black 26 reformatted 8 files.
This commit is contained in:
+28
-24
@@ -139,12 +139,16 @@ class InfluxDBAdapter(DatabaseAdapter):
|
||||
"time": measurement["timestamp"].isoformat(),
|
||||
"fields": {
|
||||
"water_level": float(measurement["water_level"]),
|
||||
"discharge": float(measurement["discharge"])
|
||||
if measurement.get("discharge") is not None
|
||||
else None,
|
||||
"discharge_percent": float(measurement["discharge_percent"])
|
||||
if measurement.get("discharge_percent")
|
||||
else None,
|
||||
"discharge": (
|
||||
float(measurement["discharge"])
|
||||
if measurement.get("discharge") is not None
|
||||
else None
|
||||
),
|
||||
"discharge_percent": (
|
||||
float(measurement["discharge_percent"])
|
||||
if measurement.get("discharge_percent")
|
||||
else None
|
||||
),
|
||||
},
|
||||
}
|
||||
points.append(point)
|
||||
@@ -551,13 +555,13 @@ class SQLAdapter(DatabaseAdapter):
|
||||
"station_code": row[1],
|
||||
"station_name_en": row[2],
|
||||
"station_name_th": row[3],
|
||||
"water_level": float(row[4])
|
||||
if row[4] is not None
|
||||
else None,
|
||||
"water_level": (
|
||||
float(row[4]) if row[4] is not None else None
|
||||
),
|
||||
"discharge": float(row[5]) if row[5] is not None else None,
|
||||
"discharge_percent": float(row[6])
|
||||
if row[6] is not None
|
||||
else None,
|
||||
"discharge_percent": (
|
||||
float(row[6]) if row[6] is not None else None
|
||||
),
|
||||
"status": row[7],
|
||||
}
|
||||
)
|
||||
@@ -611,13 +615,13 @@ class SQLAdapter(DatabaseAdapter):
|
||||
"station_code": row[1],
|
||||
"station_name_en": row[2],
|
||||
"station_name_th": row[3],
|
||||
"water_level": float(row[4])
|
||||
if row[4] is not None
|
||||
else None,
|
||||
"water_level": (
|
||||
float(row[4]) if row[4] is not None else None
|
||||
),
|
||||
"discharge": float(row[5]) if row[5] is not None else None,
|
||||
"discharge_percent": float(row[6])
|
||||
if row[6] is not None
|
||||
else None,
|
||||
"discharge_percent": (
|
||||
float(row[6]) if row[6] is not None else None
|
||||
),
|
||||
"status": row[7],
|
||||
}
|
||||
)
|
||||
@@ -666,13 +670,13 @@ class SQLAdapter(DatabaseAdapter):
|
||||
"station_id": row[1],
|
||||
"station_code": row[2] or f"Station_{row[1]}",
|
||||
"station_name_th": row[3] or f"Station {row[1]}",
|
||||
"water_level": float(row[4])
|
||||
if row[4] is not None
|
||||
else None,
|
||||
"water_level": (
|
||||
float(row[4]) if row[4] is not None else None
|
||||
),
|
||||
"discharge": float(row[5]) if row[5] is not None else None,
|
||||
"discharge_percent": float(row[6])
|
||||
if row[6] is not None
|
||||
else None,
|
||||
"discharge_percent": (
|
||||
float(row[6]) if row[6] is not None else None
|
||||
),
|
||||
"status": row[7],
|
||||
}
|
||||
)
|
||||
|
||||
+3
-3
@@ -125,9 +125,9 @@ class DatabaseHealthCheck(HealthCheck):
|
||||
"message": "Database connection OK",
|
||||
"details": {
|
||||
"latest_data_count": len(latest_data),
|
||||
"latest_timestamp": str(latest_data[0].get("timestamp"))
|
||||
if latest_data
|
||||
else None,
|
||||
"latest_timestamp": (
|
||||
str(latest_data[0].get("timestamp")) if latest_data else None
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -207,9 +207,9 @@ def fill_from_hii(
|
||||
"timestamp": missing["timestamp"],
|
||||
"station_code": code,
|
||||
"water_level": missing["wl_msl"] - offset,
|
||||
"discharge": missing["discharge"]
|
||||
if code in _HII_EXACT_MIRRORS
|
||||
else float("nan"),
|
||||
"discharge": (
|
||||
missing["discharge"] if code in _HII_EXACT_MIRRORS else float("nan")
|
||||
),
|
||||
}
|
||||
)
|
||||
fills.append(fill)
|
||||
|
||||
+6
-6
@@ -320,9 +320,9 @@ def train_station(
|
||||
skipped_heads,
|
||||
)
|
||||
else:
|
||||
skipped_heads[
|
||||
head_key
|
||||
] = f"only {n_pos} positives in train span (< {MIN_POSITIVES_FOR_CLASSIFIER})"
|
||||
skipped_heads[head_key] = (
|
||||
f"only {n_pos} positives in train span (< {MIN_POSITIVES_FOR_CLASSIFIER})"
|
||||
)
|
||||
heads[head_key] = clf
|
||||
|
||||
if not skip_eval:
|
||||
@@ -417,9 +417,9 @@ def train_station(
|
||||
if clf is not None:
|
||||
skipped_heads.pop(head_key, None)
|
||||
else:
|
||||
skipped_heads[
|
||||
head_key
|
||||
] = f"only {n_pos} positives in train span (< {MIN_POSITIVES_FOR_CLASSIFIER})"
|
||||
skipped_heads[head_key] = (
|
||||
f"only {n_pos} positives in train span (< {MIN_POSITIVES_FOR_CLASSIFIER})"
|
||||
)
|
||||
final_heads[head_key] = None
|
||||
|
||||
# v4 = + Mae Ngat dam features; v3 = rise + rain; v2 = rise target only
|
||||
|
||||
@@ -51,8 +51,7 @@ class PostgresHistory:
|
||||
if start >= end:
|
||||
raise ValueError("start must be before end")
|
||||
|
||||
query = text(
|
||||
"""
|
||||
query = text("""
|
||||
SELECT m.timestamp, s.station_code, m.water_level,
|
||||
m.discharge, m.discharge_percent
|
||||
FROM water_measurements m
|
||||
@@ -62,8 +61,7 @@ class PostgresHistory:
|
||||
AND m.timestamp <= :end_time
|
||||
ORDER BY m.timestamp ASC
|
||||
LIMIT :limit
|
||||
"""
|
||||
)
|
||||
""")
|
||||
with self.engine.connect() as connection:
|
||||
rows = connection.execute(
|
||||
query,
|
||||
@@ -91,9 +89,9 @@ class PostgresHistory:
|
||||
"station_code": station_code,
|
||||
"water_level": water_level,
|
||||
"discharge": discharge,
|
||||
"discharge_percent": float(row[4])
|
||||
if row[4] is not None
|
||||
else None,
|
||||
"discharge_percent": (
|
||||
float(row[4]) if row[4] is not None else None
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
+5
-3
@@ -173,8 +173,10 @@ class RequestTracker:
|
||||
"failed_requests": self.failed_requests,
|
||||
"success_rate": self.successful_requests / self.total_requests,
|
||||
"average_response_time": self.total_response_time / self.total_requests,
|
||||
"last_request_time": self.last_request_time.isoformat()
|
||||
if self.last_request_time
|
||||
else None,
|
||||
"last_request_time": (
|
||||
self.last_request_time.isoformat()
|
||||
if self.last_request_time
|
||||
else None
|
||||
),
|
||||
"error_breakdown": dict(self.error_count_by_type),
|
||||
}
|
||||
|
||||
+15
-9
@@ -323,9 +323,11 @@ class RidReservoirStore:
|
||||
if not preserve_cols:
|
||||
return f"INSERT OR REPLACE INTO {table} ({col_list}) VALUES ({params})"
|
||||
updates = ", ".join(
|
||||
f"{c} = COALESCE(excluded.{c}, {table}.{c})"
|
||||
if c in preserve_cols
|
||||
else f"{c} = excluded.{c}"
|
||||
(
|
||||
f"{c} = COALESCE(excluded.{c}, {table}.{c})"
|
||||
if c in preserve_cols
|
||||
else f"{c} = excluded.{c}"
|
||||
)
|
||||
for c in value_cols
|
||||
)
|
||||
return (
|
||||
@@ -334,9 +336,11 @@ class RidReservoirStore:
|
||||
)
|
||||
if self.db_type == "postgresql":
|
||||
updates = ", ".join(
|
||||
f"{c} = COALESCE(EXCLUDED.{c}, {table}.{c})"
|
||||
if c in preserve_cols
|
||||
else f"{c} = EXCLUDED.{c}"
|
||||
(
|
||||
f"{c} = COALESCE(EXCLUDED.{c}, {table}.{c})"
|
||||
if c in preserve_cols
|
||||
else f"{c} = EXCLUDED.{c}"
|
||||
)
|
||||
for c in value_cols
|
||||
)
|
||||
return (
|
||||
@@ -344,9 +348,11 @@ class RidReservoirStore:
|
||||
f"ON CONFLICT ({conflict}) DO UPDATE SET {updates}"
|
||||
)
|
||||
updates = ", ".join(
|
||||
f"{c} = COALESCE(VALUES({c}), {c})"
|
||||
if c in preserve_cols
|
||||
else f"{c} = VALUES({c})"
|
||||
(
|
||||
f"{c} = COALESCE(VALUES({c}), {c})"
|
||||
if c in preserve_cols
|
||||
else f"{c} = VALUES({c})"
|
||||
)
|
||||
for c in value_cols
|
||||
)
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user