Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
189 lines
5.8 KiB
Python
189 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.earth_news import EarthNewsItem
|
|
from app.services.earth_news import (
|
|
ParsedNewsItem,
|
|
build_anchor_location_patch,
|
|
)
|
|
|
|
|
|
def _coerce_datetime(value: datetime | None) -> datetime | None:
|
|
if value is None:
|
|
return None
|
|
if value.tzinfo is None:
|
|
return value.replace(tzinfo=UTC)
|
|
return value.astimezone(UTC)
|
|
|
|
|
|
def _location_patch_from_record(record: EarthNewsItem) -> dict[str, Any]:
|
|
return {
|
|
"latitude": record.latitude,
|
|
"longitude": record.longitude,
|
|
"location_label": record.location_label,
|
|
"location_source": record.location_source,
|
|
"verified": record.verified,
|
|
"location_meta": dict(record.location_meta or {}),
|
|
}
|
|
|
|
|
|
def record_to_parsed_news_item(record: EarthNewsItem) -> ParsedNewsItem:
|
|
return ParsedNewsItem(
|
|
id=record.id,
|
|
title=record.title,
|
|
summary=record.summary or "",
|
|
url=record.url,
|
|
source=record.source or "",
|
|
feed_name=record.feed_name or "",
|
|
feed_region=record.region or "global",
|
|
homepage_url=record.homepage_url or "",
|
|
published_at=_coerce_datetime(record.published_at),
|
|
location_patch=_location_patch_from_record(record),
|
|
)
|
|
|
|
|
|
def _query_sort_key(active_region: str):
|
|
return (
|
|
EarthNewsItem.region != active_region,
|
|
EarthNewsItem.published_at.is_(None),
|
|
EarthNewsItem.published_at.desc().nullslast(),
|
|
EarthNewsItem.feed_name.asc(),
|
|
)
|
|
|
|
|
|
async def list_earth_news_items(
|
|
db: AsyncSession,
|
|
*,
|
|
active_region: str,
|
|
limit: int,
|
|
) -> list[ParsedNewsItem]:
|
|
regions = {"global", active_region}
|
|
result = await db.execute(
|
|
select(EarthNewsItem)
|
|
.where(EarthNewsItem.region.in_(regions))
|
|
.order_by(*_query_sort_key(active_region))
|
|
.limit(limit)
|
|
)
|
|
return [record_to_parsed_news_item(record) for record in result.scalars().all()]
|
|
|
|
|
|
async def get_earth_news_freshness(
|
|
db: AsyncSession,
|
|
*,
|
|
active_region: str,
|
|
) -> tuple[int, datetime | None]:
|
|
regions = {"global", active_region}
|
|
result = await db.execute(
|
|
select(
|
|
func.count(EarthNewsItem.id),
|
|
func.max(func.coalesce(EarthNewsItem.published_at, EarthNewsItem.last_seen_at)),
|
|
).where(EarthNewsItem.region.in_(regions))
|
|
)
|
|
count, newest = result.one()
|
|
item_count = int(count or 0)
|
|
if item_count == 0:
|
|
return 0, None
|
|
return item_count, _coerce_datetime(newest)
|
|
|
|
|
|
async def upsert_earth_news_items(db: AsyncSession, items: list[ParsedNewsItem]) -> int:
|
|
if not items:
|
|
return 0
|
|
now = datetime.now(UTC)
|
|
existing_result = await db.execute(
|
|
select(EarthNewsItem).where(EarthNewsItem.id.in_([item.id for item in items]))
|
|
)
|
|
existing = {record.id: record for record in existing_result.scalars().all()}
|
|
changed = 0
|
|
for item in items:
|
|
record = existing.get(item.id)
|
|
if record is None:
|
|
patch = build_anchor_location_patch(item)
|
|
record = EarthNewsItem(
|
|
id=item.id,
|
|
title=item.title,
|
|
summary=item.summary,
|
|
url=item.url,
|
|
source=item.source,
|
|
feed_name=item.feed_name,
|
|
region=item.feed_region,
|
|
homepage_url=item.homepage_url,
|
|
published_at=item.published_at,
|
|
latitude=patch["latitude"],
|
|
longitude=patch["longitude"],
|
|
location_label=patch["location_label"],
|
|
location_source=patch["location_source"],
|
|
verified=patch["verified"],
|
|
location_meta=patch["location_meta"],
|
|
first_seen_at=now,
|
|
last_seen_at=now,
|
|
)
|
|
db.add(record)
|
|
changed += 1
|
|
continue
|
|
|
|
record.title = item.title
|
|
record.summary = item.summary
|
|
record.url = item.url
|
|
record.source = item.source
|
|
record.feed_name = item.feed_name
|
|
record.region = item.feed_region
|
|
record.homepage_url = item.homepage_url
|
|
record.published_at = item.published_at
|
|
record.last_seen_at = now
|
|
changed += 1
|
|
await db.flush()
|
|
return changed
|
|
|
|
|
|
async def update_earth_news_item_location(
|
|
db: AsyncSession,
|
|
*,
|
|
item_id: str,
|
|
patch: dict[str, Any],
|
|
) -> bool:
|
|
record = await db.get(EarthNewsItem, item_id)
|
|
if record is None:
|
|
return False
|
|
record.latitude = float(patch["latitude"])
|
|
record.longitude = float(patch["longitude"])
|
|
record.location_label = str(patch["location_label"])
|
|
record.location_source = str(patch["location_source"])
|
|
record.verified = bool(patch["verified"])
|
|
record.location_meta = dict(patch.get("location_meta") or {})
|
|
record.resolved_at = datetime.now(UTC) if record.verified else None
|
|
await db.flush()
|
|
return True
|
|
|
|
|
|
async def list_unverified_earth_news_items(
|
|
db: AsyncSession,
|
|
*,
|
|
active_region: str,
|
|
limit: int,
|
|
) -> list[ParsedNewsItem]:
|
|
regions = {"global", active_region}
|
|
result = await db.execute(
|
|
select(EarthNewsItem)
|
|
.where(EarthNewsItem.region.in_(regions))
|
|
.where(EarthNewsItem.verified.is_(False))
|
|
.order_by(*_query_sort_key(active_region))
|
|
.limit(limit)
|
|
)
|
|
return [record_to_parsed_news_item(record) for record in result.scalars().all()]
|
|
|
|
|
|
async def list_all_earth_news_records(db: AsyncSession) -> list[EarthNewsItem]:
|
|
result = await db.execute(
|
|
select(EarthNewsItem).order_by(
|
|
EarthNewsItem.published_at.desc().nullslast(),
|
|
EarthNewsItem.last_seen_at.desc(),
|
|
)
|
|
)
|
|
return list(result.scalars().all())
|