diff --git a/.gitignore b/.gitignore index 895bca1..b88fa8b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ Thumbs.db *.db nowcast-point-alarm-filter.json nowcast-point-*-backup-* +kayitlar-*.csv diff --git a/README.md b/README.md index acc4841..4e2bc89 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Nokta alarm istemcileri `BE-NowcastPointAlarm` servisinin `/v1/alarms/points` en - **Page endpoint'inde sıralama (ORDER BY) yoktur.** Tüm sayfalar gezilirken kayıtların sırası garanti değildir; bu yüzden sayfa kayması yaşanabilir (bir kayıt iki kez gelebilir, bir kayıt hiç gelmeyebilir). Backend tarafında `createdAt` ardından `id` ile sıralama önerildi (yeni kayıtlar sona eklenir, `id` eşit zamanlarda sırayı sabitler). - **Tüm kayıtlar çekilirken aynı anda register, update veya unregister yapmayın.** Veri değişirse sayfalar kayar. `nowcast-point-page.py` sonunda sunucunun bildirdiği `totalRecords` ile alınan benzersiz kayıt sayısını karşılaştırır ve fark varsa uyarı verir; bu durumda komutu yeniden çalıştırın. SQLite `registrationId` ile upsert yaptığı için tekrar çalıştırmak kayıt çoğaltmaz. -- `accountId` verilmezse ADMIN kullanıcılar tüm hesapları görür. Betik her zaman `/v1/users/me` içindeki `account.id` değerini gönderir (`--account-id` ile değiştirilebilir). Yetki yoksa (403) uyarı verip `accountId` olmadan bir kez daha dener. +- `nowcast-point-page.py` varsayılan olarak `accountId` göndermez; sunucu listeyi çağıranın kendi hesabına daraltır ve normal kullanıcılar için doğru kullanım budur. `--account-id` yalnızca en az POWER_USER yetkisiyle verilebilir; aksi halde HTTP 403 döner ve betik `--account-id` vermemenizi söyleyen bir hata yazar. ADMIN kullanıcı `accountId` vermezse tüm hesapları görür. - Backend her `RuntimeException`'ı HTTP 400 yapar; kayıt bulunamaması da 400 döner. Yalnızca gerçek 404 `NOT_FOUND` olarak işaretlenir. ### page diff --git a/nowcast-point-page.py b/nowcast-point-page.py index fd477c1..78b91d7 100644 --- a/nowcast-point-page.py +++ b/nowcast-point-page.py @@ -36,7 +36,10 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument( "--account-id", - help="Sorgulanacak hesap ID'si (varsayılan: /v1/users/me içindeki account.id)", + help=( + "Sorgulanacak hesap ID'si; yalnızca en az POWER_USER yetkisiyle verilebilir (aksi halde " + "HTTP 403). Verilmezse sunucu kendi hesabınıza daraltır; normal kullanıcılar vermemelidir" + ), ) parser.add_argument( "--recipient-id", @@ -95,19 +98,18 @@ def main() -> int: 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, + account_id=args.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) + records = store.records(None, summary.registration_ids) + webhooks = store.webhooks_of(records) if args.output: path = timestamped_path(args.output) write_records(path, records, webhooks) @@ -125,7 +127,7 @@ def main() -> int: 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) + prompt_export(store, args.account_id) except (ConfigurationError, ApiError, OSError) as exc: logging.error("İşlem durduruldu: %s", exc) return 1 diff --git a/point_alarm.py b/point_alarm.py index 965692e..0f34cdc 100644 --- a/point_alarm.py +++ b/point_alarm.py @@ -336,6 +336,11 @@ class PointStore: query += " ORDER BY recipientId, registrationId" return [dict(row) for row in self.connection.execute(query, params)] + def webhooks_of(self, records: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Webhooks of the accounts that own the given records.""" + owners = sorted({record["accountId"] for record in records if record.get("accountId")}) + return [webhook for owner in owners for webhook in self.webhooks(owner)] + def webhooks(self, account_id: str | None = None) -> list[dict[str, Any]]: if account_id: rows = self.connection.execute( @@ -432,8 +437,7 @@ def fetch_pages( ) -> FetchSummary: """Fetch one page (page_number given) or every page into the store. - A 403 for an explicit accountId (caller below POWER_USER) is retried once without - it; the server then scopes the listing to the caller's own account. + Without account_id the server scopes the listing to the caller's own account. """ summary = FetchSummary(account_id=account_id) seen: set[str] = set() @@ -443,24 +447,22 @@ def fetch_pages( data = service.page( page_number=current, page_size=page_size, - account_id=summary.account_id, + account_id=account_id, recipient_ids=recipient_ids, polygon=polygon, ) except ApiError as exc: - if exc.status_code == 403 and summary.account_id: - LOGGER.warning( - "accountId ile sorgu 403 verdi; accountId olmadan, sunucunun kendi hesabınıza " - "daraltmasıyla yeniden deneniyor." - ) - summary.account_id = None - continue + if exc.status_code == 403 and account_id: + raise ConfigurationError( + "accountId parametresi için en az POWER_USER yetkisi gerekir (HTTP 403). " + "--account-id vermeyin; sistem hesabınızı kendisi tespit eder." + ) from exc raise registrations = [item for item in (data.get("registrations") or []) if isinstance(item, dict)] summary.total_records = int(data.get("totalRecords") or 0) summary.received += len(registrations) for registration in registrations: - registration_id = store.upsert_registration(registration, summary.account_id) + registration_id = store.upsert_registration(registration, account_id) if registration_id and registration_id not in seen: seen.add(registration_id) summary.registration_ids.append(registration_id) diff --git a/tests/test_point_alarm.py b/tests/test_point_alarm.py index a715aef..4d7dbe7 100644 --- a/tests/test_point_alarm.py +++ b/tests/test_point_alarm.py @@ -97,11 +97,12 @@ class PointAlarmTest(unittest.TestCase): self.assertEqual(summary.received, 2) self.assertEqual(len(service.calls), 1) - def test_forbidden_account_retries_without_it(self) -> None: + def test_forbidden_account_gives_clear_error(self) -> None: service = FakeService([registration(1)], forbid_account=True) - summary = fetch_pages(service, self.store, account_id="acc") + with self.assertRaisesRegex(ConfigurationError, "--account-id vermeyin"): + fetch_pages(service, self.store, account_id="acc") + summary = fetch_pages(service, self.store, account_id=None) self.assertEqual(summary.distinct, 1) - self.assertIsNone(service.calls[-1]["account"]) def test_csv_and_txt_output(self) -> None: fetch_pages(FakeService([registration(1)]), self.store, account_id="acc")