feat: add nowcast point alarm page client with SQLite store

Adds point_alarm.py (flattening, secret masking, SQLite upsert store, paged fetch with 403 fallback, CSV/TXT export, shared filter loader), nowcast-point-page.py CLI and unit tests. Register will read the shared filter from nowcast-point-alarm-filter.json instead of repeating it in the CSV.
This commit is contained in:
2026-10-23 19:11:34 +03:00
parent 7a875caa73
commit 0958d26e9f
7 changed files with 813 additions and 8 deletions
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""List point alarm registrations page by page into SQLite, screen or CSV/TXT."""
from __future__ import annotations
import argparse
import logging
import sys
from pathlib import Path
from common import ApiError, ConfigurationError, IklimClient, configure_logging
from point_alarm import (
MAX_PAGE_SIZE,
PointAlarmService,
PointStore,
fetch_pages,
parse_polygon,
print_records,
prompt_export,
timestamped_path,
write_records,
)
BASE_DIR = Path(__file__).resolve().parent
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"iklim.co nokta alarm kayıtlarını /v1/alarms/points/page ile listeler. "
"Sonuçlar SQLite'a (registrationId anahtarlı) yazılır; varsayılan olarak ekrana "
"basılır, --output ile CSV/TXT'ye yazılır."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--account-id",
help="Sorgulanacak hesap ID'si (varsayılan: /v1/users/me içindeki account.id)",
)
parser.add_argument(
"--recipient-id",
action="append",
default=[],
help="Alıcı ID'sine göre süz; birden fazla kez verilebilir",
)
parser.add_argument(
"--polygon",
help="Poligon kesişim filtresi: 'lng,lat;lng,lat;...' (en az 4 nokta, kapalı halka)",
)
parser.add_argument(
"--page-number",
type=int,
help="Yalnızca bu sayfayı getir (0'dan başlar); verilmezse tüm sayfalar gezilir",
)
parser.add_argument(
"--page-size",
type=int,
default=MAX_PAGE_SIZE,
help=f"Sayfa boyutu, 1-{MAX_PAGE_SIZE} (varsayılan: {MAX_PAGE_SIZE})",
)
parser.add_argument(
"--output",
type=Path,
help=(
"Çıktı dosyası (.csv noktalı virgüllü, .txt JSON Lines); dosya adına "
"zaman soneki eklenir (dosya-YYYYmmdd-HHMMSS.csv). Verilmezse ekrana yazılır"
),
)
parser.add_argument(
"--db",
type=Path,
default=BASE_DIR / "iklim-sandbox.db",
help="SQLite dosyası (varsayılan: iklim-sandbox.db)",
)
parser.add_argument(
"--debug-log",
type=Path,
default=BASE_DIR / "logs" / "iklim-api-debug.log",
help="Maskelenmiş HTTP debug log dosyası",
)
args = parser.parse_args()
if not 1 <= args.page_size <= MAX_PAGE_SIZE:
parser.error(f"--page-size 1-{MAX_PAGE_SIZE} aralığında olmalıdır")
if args.page_number is not None and args.page_number < 0:
parser.error("--page-number 0 veya daha büyük olmalıdır")
return args
def main() -> int:
args = parse_args()
configure_logging(args.debug_log)
store = None
try:
polygon = parse_polygon(args.polygon) if args.polygon else None
client = IklimClient.from_env(BASE_DIR / ".env")
service = PointAlarmService(client)
account_id = args.account_id or service.get_account_id()
store = PointStore(args.db)
summary = fetch_pages(
service,
store,
account_id=account_id,
recipient_ids=args.recipient_id or None,
polygon=polygon,
page_size=args.page_size,
page_number=args.page_number,
)
records = store.records(summary.account_id, summary.registration_ids)
webhooks = store.webhooks(summary.account_id)
if args.output:
path = timestamped_path(args.output)
write_records(path, records, webhooks)
print(f"{len(records)} kayıt yazıldı: {path.resolve()}")
else:
print_records(records, webhooks)
if args.page_number is None and summary.missing:
logging.warning(
"Sunucu %s kayıt bildirdi, %s benzersiz kayıt alındı (%s eksik). Sayfalama sırasında "
"kayıt eklenmiş/silinmiş olabilir; komutu yeniden çalıştırın.",
summary.total_records,
summary.distinct,
summary.missing,
)
if summary.duplicates:
logging.warning("Sayfalar arasında %s tekrar eden kayıt görüldü.", summary.duplicates)
if not args.output:
prompt_export(store, summary.account_id)
except (ConfigurationError, ApiError, OSError) as exc:
logging.error("İşlem durduruldu: %s", exc)
return 1
finally:
if store:
store.close()
return 0
if __name__ == "__main__":
sys.exit(main())