Some checks failed
build-and-deploy / build (push) Failing after 4s
Vendor the newsPipeline scraper + summarizer into the repo and wire them into
docker-compose against the EXISTING osint-db (no second Postgres), replacing
the upstream k8s CronJobs with in-compose wall-clock loops (:00 scrape, :05
summarize).
- news/scraper: vendored Scrapy project (257 RSS feeds) + hourly loop
scheduler (run_news_scraper.py)
- news/summerizer: vendored Gemini map-reduce summarizer, cleaned:
* fix broken google-genai response handling (_extract_text, defensive)
* fix malformed INSERT/GRANT query in save_summary_to_db
* OSINT-neutral default MAP_PROMPT; futures/markets language gated behind
INCLUDE_FUTURES=0 (yfinance lazy-imported)
* env-configurable model, batch size, lookback window
+ hourly loop scheduler (run_news_summarizer.py, :05)
- alembic 003_news: idempotent articles + article_summaries tables
- API: GET /api/news and GET /api/news/summaries (+ models, schemas)
- tests/test_api_news.py: 5 DB-backed contract tests (all pass vs real PG)
- docs/news.md + .env.example updates
Both services run under the `ingest` compose profile (matching the
ingester/camera-scraper pattern) and build arm64 on the Pi via the existing
Forgejo CI workflow. telebot left out of scope (reserved env only).
301 lines
8.7 KiB
Python
301 lines
8.7 KiB
Python
"""OSINT Dashboard — Pydantic schemas."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# ─── Enums ───────────────────────────────────────────────────────────────
|
|
|
|
class SourceType(str, Enum):
|
|
rss = "rss"
|
|
gdel_t2 = "gdel-t2"
|
|
social = "social"
|
|
earthquake = "earthquake"
|
|
disaster = "disaster"
|
|
weather = "weather"
|
|
fire = "fire"
|
|
satellite = "satellite"
|
|
|
|
|
|
class EntityKind(str, Enum):
|
|
person = "person"
|
|
organization = "organization"
|
|
location = "location"
|
|
topic = "topic"
|
|
asset = "asset"
|
|
|
|
|
|
class Sentiment(str, Enum):
|
|
positive = "positive"
|
|
neutral = "neutral"
|
|
negative = "negative"
|
|
|
|
|
|
class AlertType(str, Enum):
|
|
entity_mention = "entity_mention"
|
|
sentiment_shift = "sentiment_shift"
|
|
geo_proximity = "geo_proximity"
|
|
keyword_match = "keyword_match"
|
|
threshold = "threshold"
|
|
anomaly = "anomaly"
|
|
|
|
|
|
class AlertSeverity(str, Enum):
|
|
low = "low"
|
|
medium = "medium"
|
|
high = "high"
|
|
critical = "critical"
|
|
|
|
|
|
# ─── Feed Sources ───────────────────────────────────────────────────────
|
|
|
|
class FeedSourceCreate(BaseModel):
|
|
name: str
|
|
source_type: SourceType
|
|
url: Optional[str] = None
|
|
config: Optional[dict] = None
|
|
|
|
|
|
class FeedSourceOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
source_type: SourceType
|
|
url: Optional[str]
|
|
config: Optional[dict]
|
|
enabled: bool
|
|
created_at: datetime
|
|
|
|
|
|
# ─── Events ─────────────────────────────────────────────────────────────
|
|
|
|
class EventCreate(BaseModel):
|
|
source_type: SourceType
|
|
source_id: Optional[UUID] = None
|
|
title: Optional[str] = None
|
|
body: Optional[str] = None
|
|
url: Optional[str] = None
|
|
sentiment_score: Optional[float] = None
|
|
sentiment_label: Optional[Sentiment] = None
|
|
location_lat: Optional[float] = None
|
|
location_lon: Optional[float] = None
|
|
location_name: Optional[str] = None
|
|
entities: Optional[list[dict]] = None
|
|
tags: Optional[list[str]] = None
|
|
raw: Optional[dict] = None
|
|
source_timestamp: Optional[datetime] = None
|
|
|
|
|
|
class EventOut(BaseModel):
|
|
id: UUID
|
|
source_type: SourceType
|
|
source_id: Optional[UUID]
|
|
title: Optional[str]
|
|
body: Optional[str]
|
|
url: Optional[str]
|
|
sentiment_score: Optional[float]
|
|
sentiment_label: Optional[Sentiment]
|
|
location_lat: Optional[float]
|
|
location_lon: Optional[float]
|
|
location_name: Optional[str]
|
|
entities: Optional[list[dict]]
|
|
tags: Optional[list[str]]
|
|
ingested_at: datetime
|
|
source_timestamp: datetime
|
|
|
|
|
|
# ─── Search ──────────────────────────────────────────────────────────────
|
|
|
|
class SearchQuery(BaseModel):
|
|
q: str = Field(..., min_length=1, max_length=500)
|
|
source_type: Optional[SourceType] = None
|
|
entity_id: Optional[UUID] = None
|
|
sentiment: Optional[Sentiment] = None
|
|
min_date: Optional[datetime] = None
|
|
max_date: Optional[datetime] = None
|
|
min_lat: Optional[float] = None
|
|
max_lat: Optional[float] = None
|
|
min_lon: Optional[float] = None
|
|
max_lon: Optional[float] = None
|
|
limit: int = Field(50, ge=1, le=500)
|
|
offset: int = Field(0, ge=0)
|
|
|
|
|
|
class SearchResult(BaseModel):
|
|
events: list[EventOut]
|
|
total: int
|
|
has_more: bool
|
|
|
|
|
|
# ─── Entities ────────────────────────────────────────────────────────────
|
|
|
|
class EntityCreate(BaseModel):
|
|
name: str
|
|
entity_type: EntityKind
|
|
aliases: Optional[list[str]] = None
|
|
description: Optional[str] = None
|
|
metadata: Optional[dict] = None
|
|
location_lat: Optional[float] = None
|
|
location_lon: Optional[float] = None
|
|
|
|
|
|
class EntityOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
entity_type: EntityKind
|
|
aliases: Optional[list[str]]
|
|
description: Optional[str]
|
|
metadata: Optional[dict]
|
|
location_lat: Optional[float]
|
|
location_lon: Optional[float]
|
|
event_count: int
|
|
first_seen: datetime
|
|
last_seen: datetime
|
|
|
|
|
|
# ─── Alerts ──────────────────────────────────────────────────────────────
|
|
|
|
class AlertCreate(BaseModel):
|
|
alert_type: AlertType
|
|
entity_id: Optional[UUID] = None
|
|
event_id: Optional[UUID] = None
|
|
severity: AlertSeverity
|
|
title: str
|
|
message: Optional[str] = None
|
|
context: Optional[dict] = None
|
|
|
|
|
|
class AlertUpdate(BaseModel):
|
|
acknowledged: Optional[bool] = None
|
|
acknowledged_by: Optional[str] = None
|
|
resolved_at: Optional[datetime] = None
|
|
|
|
|
|
class AlertOut(BaseModel):
|
|
id: UUID
|
|
alert_type: AlertType
|
|
entity_id: Optional[UUID]
|
|
event_id: Optional[UUID]
|
|
severity: AlertSeverity
|
|
title: str
|
|
message: Optional[str]
|
|
context: Optional[dict]
|
|
acknowledged: bool
|
|
acknowledged_by: Optional[str]
|
|
created_at: datetime
|
|
resolved_at: Optional[datetime]
|
|
|
|
|
|
# ─── Documents ───────────────────────────────────────────────────────────
|
|
|
|
class DocumentCreate(BaseModel):
|
|
bucket: str
|
|
object_key: str
|
|
content_type: Optional[str] = None
|
|
size_bytes: Optional[int] = None
|
|
description: Optional[str] = None
|
|
tags: Optional[list[str]] = None
|
|
event_id: Optional[UUID] = None
|
|
|
|
|
|
class DocumentOut(BaseModel):
|
|
id: UUID
|
|
bucket: str
|
|
object_key: str
|
|
content_type: Optional[str]
|
|
size_bytes: Optional[int]
|
|
description: Optional[str]
|
|
tags: Optional[list[str]]
|
|
event_id: Optional[UUID]
|
|
uploaded_at: datetime
|
|
|
|
|
|
# ─── API Keys ────────────────────────────────────────────────────────────
|
|
|
|
class KeyValueIn(BaseModel):
|
|
"""Body for POST /api/keys/{name} — the value to store.
|
|
|
|
The raw value is accepted only on write; it is never read back out.
|
|
"""
|
|
value: str = Field(..., min_length=1, max_length=4096)
|
|
|
|
|
|
class KeyOut(BaseModel):
|
|
"""One key's status as exposed by GET /api/keys.
|
|
|
|
``masked`` is the "****last4" hint; the full value is NEVER included.
|
|
"""
|
|
name: str
|
|
set: bool
|
|
masked: Optional[str] = None
|
|
description: Optional[str] = None
|
|
example: Optional[str] = None
|
|
validated: bool = True
|
|
|
|
|
|
# ─── Active fires / hotspots (NASA FIRMS) ────────────────────────────────
|
|
|
|
class FireOut(BaseModel):
|
|
latitude: float
|
|
longitude: float
|
|
brightness: float # bright_ti4 brightness temperature, Kelvin
|
|
confidence: str # VIIRS: n (nominal) / l (low) / h (high)
|
|
acq_time: datetime
|
|
satellite: str
|
|
instrument: Optional[str] = None
|
|
bright_ti5: Optional[float] = None # 12µm brightness temperature, Kelvin
|
|
frp: Optional[float] = None # fire radiative power, MW
|
|
daynight: Optional[str] = None # D / N
|
|
|
|
|
|
# ─── News pipeline (scraper + summarizer) ────────────────────────────────
|
|
|
|
class NewsArticleOut(BaseModel):
|
|
"""One scraped article as exposed by GET /api/news."""
|
|
|
|
id: int
|
|
title: Optional[str] = None
|
|
url: Optional[str] = None
|
|
content: Optional[str] = None
|
|
domain: Optional[str] = None
|
|
timestamp: Optional[datetime] = None
|
|
|
|
|
|
class NewsSummaryOut(BaseModel):
|
|
"""One master LLM summary as exposed by GET /api/news/summaries."""
|
|
|
|
id: int
|
|
summary_text: str
|
|
batch_timestamp: datetime
|
|
|
|
|
|
# ─── Aggregations ────────────────────────────────────────────────────────
|
|
|
|
class SentimentSummary(BaseModel):
|
|
period: str
|
|
positive_count: int
|
|
neutral_count: int
|
|
negative_count: int
|
|
avg_score: float
|
|
|
|
|
|
class TimelinePoint(BaseModel):
|
|
timestamp: datetime
|
|
event_count: int
|
|
avg_sentiment: float
|
|
|
|
|
|
class DashboardSummary(BaseModel):
|
|
total_events: int
|
|
events_last_24h: int
|
|
active_sources: int
|
|
open_alerts: int
|
|
tracked_entities: int
|
|
sentiment: SentimentSummary
|
|
top_entities: list[EntityOut]
|
|
|