demo_server_zone_store.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. """End-to-end demo: B1 (find_server) + B2 (bulk_upsert zones) + B3 (pick latest).
  2. This script simulates the 996box daily zone refresh and demonstrates the
  3. B1 / B2 / B3 business flow end-to-end against the real MySQL.
  4. Flow:
  5. B1: ServerStore.upsert("玖扬复古176", 14414738)
  6. → returns Server with id
  7. B2: ZoneStore.bulk_upsert(server_id, [9 zones for today])
  8. → writes 9 rows
  9. → ZoneStore.mark_missing_closed(server_id, kept_ids)
  10. (simulates zones that 996box no longer reports → close them)
  11. B3: ZoneStore.get_latest_openable(server_id)
  12. → returns the most recent zone that is status=open AND open_time <= NOW()
  13. Run:
  14. uv run python scripts/demo_server_zone_store.py
  15. uv run python scripts/demo_server_zone_store.py --cleanup
  16. """
  17. from __future__ import annotations
  18. import argparse
  19. import sys
  20. from datetime import datetime, timedelta
  21. from pathlib import Path
  22. _HERE = Path(__file__).resolve().parent
  23. sys.path.insert(0, str(_HERE.parent / "src"))
  24. from cq2_sdk.env_loader import load_env # noqa: E402
  25. _PROJECT_ROOT = _HERE.parent
  26. load_env(_PROJECT_ROOT / ".env")
  27. from cq2_sdk.db_models import Zone as DbZone # noqa: E402
  28. from cq2_sdk.store import ServerStore, ZoneStore, connection # noqa: E402
  29. TEST_SERVER = "DEMO_玖扬复古176"
  30. TEST_SERVER_ID_996BOX = 14414738
  31. GAME_ID = 206923
  32. # 9 daily time slots for 玖扬复古176 (from business-tasks.md §4 B2)
  33. TIME_SLOTS = [
  34. (0, 0), (7, 0), (10, 0), (12, 0), (14, 0),
  35. (16, 0), (19, 0), (20, 30), (21, 30),
  36. ]
  37. def build_todays_zones() -> list[DbZone]:
  38. """Construct Zone objects for today's 9 standard time slots.
  39. All marked 'open' and given open_time earlier than NOW so B3 can pick one.
  40. """
  41. now = datetime.now().replace(microsecond=0)
  42. zones: list[DbZone] = []
  43. for i, (h, m) in enumerate(TIME_SLOTS):
  44. open_time = now.replace(hour=h, minute=m, second=0)
  45. # If today's slot is in the future, use a slot from earlier today
  46. if open_time > now:
  47. open_time = now - timedelta(hours=i + 1)
  48. zones.append(DbZone(
  49. zone_id=f"slot_{h:02d}{m:02d}",
  50. zone_name=f"玖扬复古176 / {h:02d}:{m:02d}",
  51. host="jycq-206923.dhsf.xqhuyu.com",
  52. port=8001,
  53. open_time=open_time,
  54. status="open",
  55. ))
  56. return zones
  57. def main() -> int:
  58. parser = argparse.ArgumentParser(description="Demo B1/B2/B3 server+zone flow")
  59. parser.add_argument("--cleanup", action="store_true",
  60. help="Delete the test server and its zones after demo")
  61. args = parser.parse_args()
  62. print("=" * 70)
  63. print("B1/B2/B3 end-to-end demo (ServerStore + ZoneStore)")
  64. print("=" * 70)
  65. with connection() as conn:
  66. servers = ServerStore(conn)
  67. zones = ZoneStore(conn)
  68. # ---------- B1: find / upsert server ----------
  69. print("\n[B1] ServerStore.upsert(玖扬复古176, 14414738)")
  70. server = servers.upsert(TEST_SERVER, TEST_SERVER_ID_996BOX, game_id=GAME_ID)
  71. print(f" server.id = {server.id}")
  72. print(f" server.server_id = {server.server_id} (996box id)")
  73. print(f" server.first_seen_at = {server.first_seen_at}")
  74. # Read-back via both lookup methods
  75. refetched = servers.get_by_name(TEST_SERVER)
  76. assert refetched is not None and refetched.id == server.id
  77. by_996box = servers.get_by_server_id(TEST_SERVER_ID_996BOX)
  78. assert by_996box is not None and by_996box.id == server.id
  79. print(f" get_by_name -> id={refetched.id} [OK]")
  80. print(f" get_by_server_id({TEST_SERVER_ID_996BOX}) -> id={by_996box.id} [OK]")
  81. # ---------- B2: bulk_upsert today's zones ----------
  82. print(f"\n[B2] ZoneStore.bulk_upsert(server.id={server.id}, 9 zones)")
  83. todays_zones = build_todays_zones()
  84. n = zones.bulk_upsert(server.id, todays_zones)
  85. print(f" rows affected: {n}")
  86. # Simulate 996box removing one zone: drop "slot_0700" and refresh
  87. kept_ids = {z.zone_id for z in todays_zones if z.zone_id != "slot_0700"}
  88. closed = zones.mark_missing_closed(server.id, kept_ids)
  89. print(f" mark_missing_closed (simulated 996box dropped 'slot_0700'): "
  90. f"{closed} zone(s) flipped to 'closed'")
  91. # Verify today's open list
  92. todays = zones.get_today_open(server.id)
  93. print(f" get_today_open → {len(todays)} zone(s) for today")
  94. for z in todays[:5]:
  95. print(f" - {z.zone_id} open={z.open_time} status={z.status}")
  96. if len(todays) > 5:
  97. print(f" ... ({len(todays) - 5} more)")
  98. # ---------- B3: pick the latest openable zone ----------
  99. print("\n[B3] ZoneStore.get_latest_openable(server.id)")
  100. latest = zones.get_latest_openable(server.id)
  101. if latest is None:
  102. print(" no openable zone found (B3 would raise NoZoneAvailable)")
  103. else:
  104. print(f" latest.zone_id = {latest.zone_id}")
  105. print(f" latest.open_time = {latest.open_time}")
  106. print(f" latest.status = {latest.status}")
  107. print(f" latest.host:port = {latest.host}:{latest.port}")
  108. # ---------- summary ----------
  109. print("\n[summary] persisted state in MySQL 996cq:")
  110. print(f" servers.name = '{server.server_name}' id={server.id}")
  111. print(f" zones = {len(todays)} today, {n} total upserted")
  112. if args.cleanup:
  113. print("\n[cleanup] deleting test server and zones...")
  114. with conn.cursor() as cur:
  115. cur.execute("DELETE FROM zones WHERE server_id = %s", (server.id,))
  116. cur.execute("DELETE FROM servers WHERE id = %s", (server.id,))
  117. print(" done")
  118. else:
  119. print("\n (use --cleanup to delete the test rows)")
  120. print("\n[OK] demo complete.")
  121. return 0
  122. if __name__ == "__main__":
  123. sys.exit(main())