release: bump version to 0.61.0
This commit is contained in:
@@ -56,6 +56,13 @@ from app.services.vessel_ais_aggregation import (
|
||||
get_vessel_raw_observations,
|
||||
MAX_SNAPSHOT_LIMIT,
|
||||
)
|
||||
from app.services.earth_layer_cache import (
|
||||
EarthLayerCachePolicy,
|
||||
earth_layer_cache,
|
||||
format_bbox_key,
|
||||
get_or_build_layer_payload,
|
||||
quantize_bbox,
|
||||
)
|
||||
from app.core.logging import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
@@ -69,6 +76,68 @@ TERRAIN_TILE_BATCH_CONCURRENCY = 16
|
||||
_terrain_tile_cache: OrderedDict[tuple[int, int, int], tuple[bytes, str, dict[str, str]]] = OrderedDict()
|
||||
VESSEL_NAME_FALLBACK_PATTERN = re.compile(r"^mmsi\s*\d+$", re.IGNORECASE)
|
||||
VESSEL_SNAPSHOT_LEGACY_FALLBACK_ENABLED = True
|
||||
SECONDS_PER_MINUTE = 60
|
||||
BYTES_PER_MIB = 1024 * 1024
|
||||
CABLE_CACHE_FRESH_SECONDS = 6 * 60 * SECONDS_PER_MINUTE
|
||||
CABLE_CACHE_STALE_SECONDS = 24 * 60 * SECONDS_PER_MINUTE
|
||||
SATELLITE_CACHE_FRESH_SECONDS = 15 * SECONDS_PER_MINUTE
|
||||
SATELLITE_CACHE_STALE_SECONDS = 2 * 60 * SECONDS_PER_MINUTE
|
||||
COMPUTE_CENTER_CACHE_FRESH_SECONDS = 10 * SECONDS_PER_MINUTE
|
||||
COMPUTE_CENTER_CACHE_STALE_SECONDS = 60 * SECONDS_PER_MINUTE
|
||||
BGP_CACHE_FRESH_SECONDS = 60
|
||||
BGP_EVENT_CACHE_FRESH_SECONDS = 30
|
||||
BGP_CACHE_STALE_SECONDS = 10 * SECONDS_PER_MINUTE
|
||||
VESSEL_SNAPSHOT_CACHE_FRESH_SECONDS = 5
|
||||
VESSEL_SNAPSHOT_CACHE_STALE_SECONDS = 30
|
||||
|
||||
CABLE_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
CABLE_CACHE_FRESH_SECONDS,
|
||||
CABLE_CACHE_STALE_SECONDS,
|
||||
max_features=6000,
|
||||
max_bytes=10 * BYTES_PER_MIB,
|
||||
)
|
||||
LANDING_POINT_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
CABLE_CACHE_FRESH_SECONDS,
|
||||
CABLE_CACHE_STALE_SECONDS,
|
||||
max_features=6000,
|
||||
max_bytes=8 * BYTES_PER_MIB,
|
||||
)
|
||||
SATELLITE_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
SATELLITE_CACHE_FRESH_SECONDS,
|
||||
SATELLITE_CACHE_STALE_SECONDS,
|
||||
max_features=8000,
|
||||
max_bytes=10 * BYTES_PER_MIB,
|
||||
)
|
||||
COMPUTE_CENTER_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
COMPUTE_CENTER_CACHE_FRESH_SECONDS,
|
||||
COMPUTE_CENTER_CACHE_STALE_SECONDS,
|
||||
max_features=1000,
|
||||
max_bytes=4 * BYTES_PER_MIB,
|
||||
)
|
||||
BGP_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
BGP_CACHE_FRESH_SECONDS,
|
||||
BGP_CACHE_STALE_SECONDS,
|
||||
max_features=1000,
|
||||
max_bytes=3 * BYTES_PER_MIB,
|
||||
)
|
||||
BGP_EVENT_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
BGP_EVENT_CACHE_FRESH_SECONDS,
|
||||
BGP_CACHE_STALE_SECONDS,
|
||||
max_features=1000,
|
||||
max_bytes=3 * BYTES_PER_MIB,
|
||||
)
|
||||
SUMMARY_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
BGP_EVENT_CACHE_FRESH_SECONDS,
|
||||
BGP_CACHE_STALE_SECONDS,
|
||||
max_features=0,
|
||||
max_bytes=512 * 1024,
|
||||
)
|
||||
VESSEL_SNAPSHOT_CACHE_POLICY = EarthLayerCachePolicy(
|
||||
VESSEL_SNAPSHOT_CACHE_FRESH_SECONDS,
|
||||
VESSEL_SNAPSHOT_CACHE_STALE_SECONDS,
|
||||
max_features=1500,
|
||||
max_bytes=3 * BYTES_PER_MIB,
|
||||
)
|
||||
|
||||
|
||||
class TerrariumTileRequest(BaseModel):
|
||||
@@ -1010,7 +1079,40 @@ async def build_vessel_snapshot_response(
|
||||
type_filter: str | None,
|
||||
limit: int | None,
|
||||
since_minutes: int = 60,
|
||||
response: Response | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if use_cache and bbox is not None:
|
||||
safe_limit_for_key = _safe_vessel_limit(limit)
|
||||
safe_since_for_key = min(max(int(since_minutes or 60), 1), 1440)
|
||||
cache_key = earth_layer_cache.key(
|
||||
"vessels-snapshot",
|
||||
bbox=format_bbox_key(quantize_bbox(bbox)),
|
||||
zoom=zoom or "none",
|
||||
type=type_filter or "all",
|
||||
limit=safe_limit_for_key,
|
||||
since=safe_since_for_key,
|
||||
)
|
||||
|
||||
async def build_uncached() -> dict[str, Any]:
|
||||
return await build_vessel_snapshot_response(
|
||||
db,
|
||||
bbox=bbox,
|
||||
zoom=zoom,
|
||||
type_filter=type_filter,
|
||||
limit=limit,
|
||||
since_minutes=since_minutes,
|
||||
response=None,
|
||||
use_cache=False,
|
||||
)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=cache_key,
|
||||
policy=VESSEL_SNAPSHOT_CACHE_POLICY,
|
||||
builder=build_uncached,
|
||||
response=response,
|
||||
)
|
||||
|
||||
requested_types = _requested_vessel_types(type_filter)
|
||||
safe_limit = _safe_vessel_limit(limit)
|
||||
safe_since_minutes = min(max(int(since_minutes or 60), 1), 1440)
|
||||
@@ -1444,8 +1546,20 @@ def convert_bgp_incidents_to_geojson(
|
||||
|
||||
|
||||
@router.get("/geo/cables")
|
||||
async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
|
||||
async def get_cables_geojson(db: AsyncSession = Depends(get_db), response: Response = None):
|
||||
"""获取海底电缆 GeoJSON 数据 (LineString)"""
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_cables_geojson(db)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("cables"),
|
||||
policy=CABLE_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_cables_geojson(db: AsyncSession) -> dict[str, Any]:
|
||||
try:
|
||||
records = await _load_current_collected_data(db, "arcgis_cables")
|
||||
|
||||
@@ -1478,7 +1592,19 @@ async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/geo/landing-points")
|
||||
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
||||
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db), response: Response = None):
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_landing_points_geojson(db)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("landing-points"),
|
||||
policy=LANDING_POINT_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_landing_points_geojson(db: AsyncSession) -> dict[str, Any]:
|
||||
try:
|
||||
records_by_source = await _load_current_collected_data_by_sources(
|
||||
db,
|
||||
@@ -1731,8 +1857,25 @@ async def get_satellites_geojson(
|
||||
description="Maximum number of satellites to return. Omit for no limit.",
|
||||
),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
response: Response = None,
|
||||
):
|
||||
"""获取卫星 TLE GeoJSON 数据"""
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_satellites_geojson(limit=limit, db=db)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("satellites", limit=limit or "all"),
|
||||
policy=SATELLITE_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_satellites_geojson(
|
||||
*,
|
||||
limit: int | None,
|
||||
db: AsyncSession,
|
||||
) -> dict[str, Any]:
|
||||
records = await _load_current_or_latest_task_data(
|
||||
db,
|
||||
"celestrak_tle",
|
||||
@@ -1800,8 +1943,25 @@ async def get_gpu_clusters_geojson(
|
||||
async def get_compute_centers_geojson(
|
||||
limit: int = Query(200, ge=1, le=1000),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
response: Response = None,
|
||||
):
|
||||
"""获取统一算力中心 GeoJSON 数据"""
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_compute_centers_geojson(limit=limit, db=db)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("compute-centers", limit=limit),
|
||||
policy=COMPUTE_CENTER_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_compute_centers_geojson(
|
||||
*,
|
||||
limit: int,
|
||||
db: AsyncSession,
|
||||
) -> dict[str, Any]:
|
||||
records_by_source = await _load_current_collected_data_by_sources(
|
||||
db,
|
||||
["top500", "epoch_ai_gpu"],
|
||||
@@ -2397,7 +2557,31 @@ async def get_bgp_anomalies_geojson(
|
||||
status: Optional[str] = Query("active"),
|
||||
limit: int = Query(200, ge=1, le=1000),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
response: Response = None,
|
||||
):
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_bgp_anomalies_geojson(
|
||||
severity=severity,
|
||||
status=status,
|
||||
limit=limit,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("bgp-anomalies", severity=severity or "all", status=status or "all", limit=limit),
|
||||
policy=BGP_EVENT_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_bgp_anomalies_geojson(
|
||||
*,
|
||||
severity: str | None,
|
||||
status: str | None,
|
||||
limit: int,
|
||||
db: AsyncSession,
|
||||
) -> dict[str, Any]:
|
||||
stmt = select(BGPAnomaly).order_by(BGPAnomaly.created_at.desc()).limit(limit)
|
||||
if severity:
|
||||
stmt = stmt.where(BGPAnomaly.severity == severity)
|
||||
@@ -2417,7 +2601,31 @@ async def get_bgp_incidents_geojson(
|
||||
status: Optional[str] = Query("active"),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
response: Response = None,
|
||||
):
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_bgp_incidents_geojson(
|
||||
severity=severity,
|
||||
status=status,
|
||||
limit=limit,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("bgp-incidents", severity=severity or "all", status=status or "all", limit=limit),
|
||||
policy=BGP_EVENT_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_bgp_incidents_geojson(
|
||||
*,
|
||||
severity: str | None,
|
||||
status: str | None,
|
||||
limit: int,
|
||||
db: AsyncSession,
|
||||
) -> dict[str, Any]:
|
||||
stmt = select(BGPIncident).order_by(BGPIncident.created_at.desc()).limit(limit)
|
||||
if severity:
|
||||
stmt = stmt.where(BGPIncident.severity == severity)
|
||||
@@ -2432,7 +2640,19 @@ async def get_bgp_incidents_geojson(
|
||||
|
||||
|
||||
@router.get("/geo/bgp-collectors")
|
||||
async def get_bgp_collectors_geojson(db: AsyncSession = Depends(get_db)):
|
||||
async def get_bgp_collectors_geojson(db: AsyncSession = Depends(get_db), response: Response = None):
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_bgp_collectors_geojson(db)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("bgp-collectors"),
|
||||
policy=BGP_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_bgp_collectors_geojson(db: AsyncSession) -> dict[str, Any]:
|
||||
coverage = await build_bgp_collector_coverage(
|
||||
db,
|
||||
source_filter=("ris_live_bgp", "bgpstream_bgp"),
|
||||
@@ -2447,8 +2667,20 @@ async def get_bgp_collectors_geojson(db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/geo/summary")
|
||||
async def get_visualization_geo_summary(db: AsyncSession = Depends(get_db)):
|
||||
async def get_visualization_geo_summary(db: AsyncSession = Depends(get_db), response: Response = None):
|
||||
"""Return lightweight Earth HUD counts without loading layer GeoJSON payloads."""
|
||||
async def build_payload() -> dict[str, Any]:
|
||||
return await _build_visualization_geo_summary(db)
|
||||
|
||||
return await get_or_build_layer_payload(
|
||||
key=earth_layer_cache.key("summary"),
|
||||
policy=SUMMARY_CACHE_POLICY,
|
||||
builder=build_payload,
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
async def _build_visualization_geo_summary(db: AsyncSession) -> dict[str, Any]:
|
||||
cable_count = await _count_current_or_latest_task_data(db, "arcgis_cables")
|
||||
landing_point_count = await _count_current_or_latest_task_data(db, "arcgis_landing_points")
|
||||
satellite_count = await _count_current_or_latest_task_data(
|
||||
|
||||
Reference in New Issue
Block a user