DinoPLayZ commited on
Commit
f527f8e
·
verified ·
1 Parent(s): 91eab63

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +247 -81
main.py CHANGED
@@ -2,20 +2,95 @@ import os
2
  import time
3
  import argparse
4
  import requests
 
 
 
5
  from datetime import datetime
6
  from dotenv import load_dotenv
7
  import threading
8
- from http.server import BaseHTTPRequestHandler, HTTPServer
 
 
 
 
 
 
 
 
 
9
 
10
  # Load optional .env if present in same directory
11
  load_dotenv()
12
 
 
 
 
 
 
 
 
 
 
 
13
  # ==============================================================================
14
  # CONFIGURATION
15
  # ==============================================================================
16
  # Environment values (Make sure to populate your .env file)
17
  # We will reload these inside the loop so you can change them on the fly.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  # Email settings (Loaded from .env)
20
  # The email you are sending FROM and TO (can be the same)
21
  EMAIL_ADDRESS = os.getenv("EMAIL_ADDRESS", "")
@@ -49,12 +124,48 @@ from email.mime.text import MIMEText
49
  from email.mime.multipart import MIMEMultipart
50
 
51
  def send_email_message(subject: str, body: str, is_html=False):
52
- print(f"\n[DEBUG] --> send_email_message called")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  if not EMAIL_ADDRESS or not EMAIL_PASSWORD or not EMAIL_RECIPIENT:
54
- print("\n[!] Email credentials not configured. The following alert would have been sent:\n")
55
- print(f"Subject: {subject}")
56
- print(body)
57
- print("-" * 50)
58
  return False
59
 
60
  try:
@@ -74,11 +185,10 @@ def send_email_message(subject: str, body: str, is_html=False):
74
  text = msg.as_string()
75
  server.sendmail(EMAIL_ADDRESS, EMAIL_RECIPIENT, text)
76
  server.quit()
77
- print(f"[DEBUG] Email sent successfully.")
78
  return True
79
  except Exception as e:
80
- print(f"[DEBUG] Network/SMTP EXCEPTION: {e}")
81
- print(f"[!] Failed to send email: {e}")
82
  return False
83
 
84
  def send_event_alerts(events):
@@ -117,25 +227,47 @@ def send_event_alerts(events):
117
  # SCRAPER LOGIC
118
  # ==============================================================================
119
  def fetch_bip_events(xsrf_token, bip_session, page=1):
120
- print(f"\n[DEBUG] --> fetch_bip_events(page={page})")
 
121
  cookies = {
122
  "XSRF-TOKEN": xsrf_token,
123
  "bip_session": bip_session
124
  }
125
  params = {"perPage": 10, "page": page}
126
- try:
127
- print(f"[DEBUG] Fetching strictly from {BIP_API} ...")
128
- r = requests.get(BIP_API, params=params, headers=HEADERS, cookies=cookies, timeout=20)
129
- print(f"[DEBUG] BIP API responded with HTTP {r.status_code}")
130
- # Check if session expired based on HTML redirect instead of JSON
131
- if "text/html" in r.headers.get("Content-Type", "") or r.status_code == 401 or r.status_code == 403:
132
- print(f"[DEBUG] Session expired detected! Content-type: {r.headers.get('Content-Type')}, Status: {r.status_code}")
133
- return None, "Session expired or invalid cookies."
134
- r.raise_for_status()
135
- return r.json(), None
136
- except Exception as e:
137
- print(f"[DEBUG] Network/Request EXCEPTION in fetch_bip_events: {type(e).__name__} - {e}")
138
- return None, str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  def parse_event(resource):
141
  data = {}
@@ -182,35 +314,34 @@ def check_new_events(last_id, xsrf_token, bip_session):
182
  # SCHEDULER ENGINE
183
  # ==============================================================================
184
  def process_tick():
185
- global LAST_EVENT_ID, LAST_EVENT_CODE, SESSION_EXPIRED, EXPIRED_XSRF, EXPIRED_BIP
186
- print("\n[DEBUG] --- process_tick starting ---")
187
- # Reload environment variables on every tick so user can update .env without restarting
 
188
  load_dotenv(override=True)
189
  xsrf = os.getenv("XSRF_TOKEN", "")
190
  bip = os.getenv("BIP_SESSION", "")
191
 
192
- now = datetime.now()
193
- time_str = now.strftime('%I:%M:%S %p')
194
-
195
  if not xsrf or not bip:
196
- print(f"[{time_str}] ⏳ Skipping check: Please configure XSRF_TOKEN and BIP_SESSION in the .env file.")
197
  return
198
 
199
- # Unsilenced print block to report resuming checking!
200
  if SESSION_EXPIRED:
201
  if xsrf == EXPIRED_XSRF and bip == EXPIRED_BIP:
202
- print(f"{time_str.lower()} - ⏸️ Paused: Waiting for new cookies in .env to resume...")
203
  return
204
  else:
205
- print(f"{time_str.lower()} - 🔄 New cookies detected! Resuming checks...")
206
  SESSION_EXPIRED = False
207
- EXPIRED_XSRF = None
208
- EXPIRED_BIP = None
 
 
209
 
210
  new_events, err = check_new_events(LAST_EVENT_ID, xsrf, bip)
211
 
212
  if err:
213
- print(f"{time_str.lower()} - ❌ Error scraping events: {err}")
214
  send_email_message(
215
  "⚠️ BIP Scraper Error",
216
  "⚠️ <b>Scraper Error!</b><br><br>"
@@ -220,8 +351,6 @@ def process_tick():
220
  is_html=True
221
  )
222
  SESSION_EXPIRED = True
223
- EXPIRED_XSRF = xsrf
224
- EXPIRED_BIP = bip
225
  return
226
 
227
  if new_events:
@@ -229,21 +358,23 @@ def process_tick():
229
  if LAST_EVENT_ID is None:
230
  LAST_EVENT_ID = new_events[0]["id"]
231
  LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
232
- print(f"{time_str.lower()} - EVENT ID : {LAST_EVENT_CODE} (Tracking started)")
 
233
  else:
234
  send_event_alerts(new_events)
235
  LAST_EVENT_ID = new_events[0]["id"]
236
  LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
 
 
237
  for ev in new_events:
238
  code = ev.get('event_code', ev['id'])
239
- print(f"{time_str.lower()} - 🚨 NEW EVENT ID : {code} (Alert Sent!)")
240
  else:
241
- # Just print the tracking status format exactly as requested on every 1-minute tick
242
- print(f"{time_str.lower()} - EVENT ID : {LAST_EVENT_CODE}")
243
 
244
  def list_all_events():
245
  """Fetches the first page of events from BIP and prints them."""
246
- print("Fetching recent events from BIP...")
247
 
248
  load_dotenv(override=True)
249
  xsrf = os.getenv("XSRF_TOKEN", "")
@@ -251,15 +382,15 @@ def list_all_events():
251
 
252
  data, err = fetch_bip_events(xsrf, bip, page=1)
253
  if err:
254
- print(f"Error: {err}")
255
  return
256
 
257
  resources = data.get("resources", [])
258
  if not resources:
259
- print("No events found.")
260
  return
261
 
262
- print(f"\nFound {len(resources)} recent events:")
263
  print("-" * 60)
264
  for res in resources:
265
  ev = parse_event(res)
@@ -268,7 +399,7 @@ def list_all_events():
268
 
269
  def get_latest_event():
270
  """Fetches and prints only the single most recent event."""
271
- print("Fetching the latest event...")
272
 
273
  load_dotenv(override=True)
274
  xsrf = os.getenv("XSRF_TOKEN", "")
@@ -276,12 +407,12 @@ def get_latest_event():
276
 
277
  data, err = fetch_bip_events(xsrf, bip, page=1)
278
  if err:
279
- print(f"Error: {err}")
280
  return
281
 
282
  resources = data.get("resources", [])
283
  if not resources:
284
- print("No events found.")
285
  return
286
 
287
  ev = parse_event(resources[0])
@@ -298,18 +429,18 @@ def get_latest_event():
298
 
299
  def test_email_alert():
300
  """Sends a dummy test message to the configured Email."""
301
- print("Sending test exact alert to Email...")
302
- success = send_email_message("🤖 Test Alert", "🤖 <b>Test Alert from BIP CLI Notifier</b><br><br>Your Email integration is working perfectly!", is_html=True)
303
  if success:
304
- print("✅ Test message sent successfully!")
305
  else:
306
- print("❌ Failed to send test message. Check your .env configuration.")
307
 
308
  def start_loop():
309
- print("=" * 60)
310
- print("🚀 BIP CLI Notifier Started")
311
- print("=" * 60)
312
- print("Press Ctrl+C to stop the notifier at any time.\n")
313
 
314
  try:
315
  while True:
@@ -324,37 +455,71 @@ def start_loop():
324
  if remaining > 0:
325
  time.sleep(min(5, remaining))
326
  except KeyboardInterrupt:
327
- print("\n\n🛑 Notifier manually stopped by user. Goodbye!")
 
 
 
 
 
 
 
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
- def run_dummy_server():
 
331
  """
332
- Hugging Face Spaces requires a web server listening on port 7860 to pass health checks.
333
- This tiny server does nothing but say "I am alive" in the background.
334
  """
335
- class DummyHandler(BaseHTTPRequestHandler):
336
- def do_GET(self):
337
- self.send_response(200)
338
- self.send_header('Content-type', 'text/html')
339
- self.end_headers()
340
- self.wfile.write(b"BIP Notifier is running in the background!")
341
-
342
- def log_message(self, format, *args):
343
- pass # Keep terminal clean from HTTP logs
344
-
345
- server = HTTPServer(('0.0.0.0', 7860), DummyHandler)
346
- server.serve_forever()
 
347
 
 
 
 
 
 
 
 
 
 
 
348
 
349
  if __name__ == "__main__":
350
- print("Starting BIP CLI Notifier...")
351
  parser = argparse.ArgumentParser(description="BIP Cloud Notifier CLI")
352
  parser.add_argument("--list-all", action="store_true", help="List all recent events and exit")
353
  parser.add_argument("--latest", action="store_true", help="Print details of the latest event and exit")
354
  parser.add_argument("--test-alert", action="store_true", help="Send a test message to Email and exit")
355
- parser.add_argument("--run", action="store_true", help="Start the continuous 1-minute monitoring loop")
356
- print("Parsed arguments:", parser.parse_args())
 
357
  args = parser.parse_args()
 
358
 
359
  if args.list_all:
360
  list_all_events()
@@ -363,9 +528,10 @@ if __name__ == "__main__":
363
  elif args.test_alert:
364
  test_email_alert()
365
  elif args.run:
366
- threading.Thread(target=run_dummy_server, daemon=True).start()
367
- start_loop()
 
368
  else:
369
- # If no arguments provided, default to starting the loop just like before
370
- threading.Thread(target=run_dummy_server, daemon=True).start()
371
- start_loop()
 
2
  import time
3
  import argparse
4
  import requests
5
+ import random
6
+ import logging
7
+ import json
8
  from datetime import datetime
9
  from dotenv import load_dotenv
10
  import threading
11
+ import asyncio
12
+
13
+ from fastapi import FastAPI, BackgroundTasks
14
+ import uvicorn
15
+
16
+ # Task 4 & 5 dependencies (Sendgrid API & Logging)
17
+ from urllib.error import HTTPError
18
+
19
+ # Task 4 & 5 dependencies (Sendgrid API & Logging)
20
+ from urllib.error import HTTPError
21
 
22
  # Load optional .env if present in same directory
23
  load_dotenv()
24
 
25
+ # ==============================================================================
26
+ # LOGGING (Task 5)
27
+ # ==============================================================================
28
+ logging.basicConfig(
29
+ level=logging.INFO,
30
+ format='[%(asctime)s] %(levelname)s: %(message)s',
31
+ datefmt='%I:%M:%S %p'
32
+ )
33
+ logger = logging.getLogger(__name__)
34
+
35
  # ==============================================================================
36
  # CONFIGURATION
37
  # ==============================================================================
38
  # Environment values (Make sure to populate your .env file)
39
  # We will reload these inside the loop so you can change them on the fly.
40
 
41
+ # Email settings
42
+ EMAIL_ADDRESS = os.getenv("EMAIL_ADDRESS", "")
43
+ EMAIL_RECIPIENT = os.getenv("EMAIL_RECIPIENT", EMAIL_ADDRESS)
44
+ EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD", "")
45
+ BREVO_API_KEY = os.getenv("BREVO_API_KEY", "")
46
+
47
+ # App check interval in seconds (default 60 secs = 1 min)
48
+ CHECK_INTERVAL_SECONDS = int(os.getenv("CHECK_INTERVAL_SECONDS", "60"))
49
+
50
+ BIP_API = "https://bip.bitsathy.ac.in/nova-api/student-activity-masters"
51
+ HEADERS = {
52
+ "accept": "application/json",
53
+ "x-requested-with": "XMLHttpRequest",
54
+ }
55
+
56
+ # ==============================================================================
57
+ # GLOBAL SESSION (Task 2)
58
+ # ==============================================================================
59
+ SESSION = requests.Session()
60
+ SESSION.headers.update(HEADERS)
61
+
62
+ # State tracking for the loop
63
+ STATE_FILE = "state.txt"
64
+ LAST_EVENT_ID = None
65
+ LAST_EVENT_CODE = None
66
+ SESSION_EXPIRED = False
67
+ EXPIRED_XSRF = None
68
+ EXPIRED_BIP = None
69
+
70
+ # ==============================================================================
71
+ # STATE MANAGEMENT (Task 1)
72
+ # ==============================================================================
73
+ def load_state():
74
+ global LAST_EVENT_ID
75
+ if os.path.exists(STATE_FILE):
76
+ try:
77
+ with open(STATE_FILE, "r") as f:
78
+ content = f.read().strip()
79
+ if content:
80
+ LAST_EVENT_ID = int(content)
81
+ logger.info(f"Loaded LAST_EVENT_ID from state: {LAST_EVENT_ID}")
82
+ except Exception as e:
83
+ logger.error(f"Failed to read state file: {e}")
84
+
85
+ def save_state(event_id):
86
+ try:
87
+ with open(STATE_FILE, "w") as f:
88
+ f.write(str(event_id))
89
+ except Exception as e:
90
+ logger.error(f"Failed to write state file: {e}")
91
+ # Environment values (Make sure to populate your .env file)
92
+ # We will reload these inside the loop so you can change them on the fly.
93
+
94
  # Email settings (Loaded from .env)
95
  # The email you are sending FROM and TO (can be the same)
96
  EMAIL_ADDRESS = os.getenv("EMAIL_ADDRESS", "")
 
124
  from email.mime.multipart import MIMEMultipart
125
 
126
  def send_email_message(subject: str, body: str, is_html=False):
127
+ # Cloud Safe Alternative (Brevo API)
128
+ if BREVO_API_KEY:
129
+ logger.debug("--> send_email_message called via Brevo API")
130
+ try:
131
+ url = "https://api.brevo.com/v3/smtp/email"
132
+ headers = {
133
+ "accept": "application/json",
134
+ "api-key": BREVO_API_KEY,
135
+ "content-type": "application/json"
136
+ }
137
+
138
+ payload = {
139
+ "sender": {"name": "BIP Auto Notifier", "email": EMAIL_ADDRESS},
140
+ "to": [{"email": EMAIL_RECIPIENT}],
141
+ "subject": subject
142
+ }
143
+
144
+ if is_html:
145
+ payload["htmlContent"] = body
146
+ else:
147
+ payload["textContent"] = body
148
+
149
+ response = requests.post(url, json=payload, headers=headers, timeout=10)
150
+
151
+ if response.status_code in [201, 200, 202]:
152
+ logger.info(f"Brevo email sent successfully! Status: {response.status_code}")
153
+ return True
154
+ else:
155
+ try:
156
+ error_data = response.json()
157
+ logger.error(f"Brevo API Error (Status {response.status_code}): {error_data.get('message', 'Unknown error')} - Code: {error_data.get('code', 'Unknown')}")
158
+ except Exception:
159
+ logger.error(f"Brevo API Error: {response.status_code} - {response.text}")
160
+ return False
161
+ except Exception as e:
162
+ logger.error(f"Brevo Network EXCEPTION: {e}")
163
+ return False
164
+
165
+ # Local Fallback (SMTP)
166
+ logger.debug("--> send_email_message called via Standard SMTP")
167
  if not EMAIL_ADDRESS or not EMAIL_PASSWORD or not EMAIL_RECIPIENT:
168
+ logger.warning(f"Email credentials not configured. The following alert '{subject}' would have been sent.")
 
 
 
169
  return False
170
 
171
  try:
 
185
  text = msg.as_string()
186
  server.sendmail(EMAIL_ADDRESS, EMAIL_RECIPIENT, text)
187
  server.quit()
188
+ logger.info(f"SMTP Email sent successfully.")
189
  return True
190
  except Exception as e:
191
+ logger.error(f"Network/SMTP EXCEPTION: {e}")
 
192
  return False
193
 
194
  def send_event_alerts(events):
 
227
  # SCRAPER LOGIC
228
  # ==============================================================================
229
  def fetch_bip_events(xsrf_token, bip_session, page=1):
230
+ logger.debug(f"--> fetch_bip_events(page={page})")
231
+
232
  cookies = {
233
  "XSRF-TOKEN": xsrf_token,
234
  "bip_session": bip_session
235
  }
236
  params = {"perPage": 10, "page": page}
237
+
238
+ # Task 3: Exponential Backoff Retry Logic
239
+ max_retries = 3
240
+ for attempt in range(max_retries):
241
+ try:
242
+ # Task 2: Use global requests.Session instead of bare requests.get
243
+ r = SESSION.get(BIP_API, params=params, cookies=cookies, timeout=20)
244
+
245
+ # Check for session expiration
246
+ if "text/html" in r.headers.get("Content-Type", "") or r.status_code in [401, 403]:
247
+ logger.warning(f"Session expired detected! Content-type: {r.headers.get('Content-Type')}, Status: {r.status_code}")
248
+ return None, "Session expired or invalid cookies."
249
+
250
+ r.raise_for_status()
251
+
252
+ # Task 7: Response format validation
253
+ data = r.json()
254
+ if not isinstance(data, dict) or "resources" not in data or not isinstance(data["resources"], list):
255
+ if attempt < max_retries - 1:
256
+ raise Exception("Invalid JSON structure received from BIP API: missing or malformed 'resources' list")
257
+ return None, "Invalid JSON structure received from BIP API."
258
+
259
+ return data, None
260
+
261
+ except Exception as e:
262
+ if attempt < max_retries - 1:
263
+ sleep_time = (2 ** attempt) + random.uniform(0.1, 1.0)
264
+ logger.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {sleep_time:.2f}s...")
265
+ time.sleep(sleep_time)
266
+ else:
267
+ logger.error(f"Network/Request EXCEPTION in fetch_bip_events after {max_retries} attempts: {e}")
268
+ return None, str(e)
269
+
270
+ return None, "Max retries exceeded."
271
 
272
  def parse_event(resource):
273
  data = {}
 
314
  # SCHEDULER ENGINE
315
  # ==============================================================================
316
  def process_tick():
317
+ global LAST_EVENT_ID, LAST_EVENT_CODE, SESSION_EXPIRED
318
+ logger.debug("--- process_tick starting ---")
319
+
320
+ # Reload environment variables on every tick
321
  load_dotenv(override=True)
322
  xsrf = os.getenv("XSRF_TOKEN", "")
323
  bip = os.getenv("BIP_SESSION", "")
324
 
 
 
 
325
  if not xsrf or not bip:
326
+ logger.warning("Skipping check: Please configure XSRF_TOKEN and BIP_SESSION in the .env file.")
327
  return
328
 
 
329
  if SESSION_EXPIRED:
330
  if xsrf == EXPIRED_XSRF and bip == EXPIRED_BIP:
331
+ logger.info("Paused: Waiting for new cookies in .env to resume...")
332
  return
333
  else:
334
+ logger.info("New cookies detected! Resuming checks...")
335
  SESSION_EXPIRED = False
336
+
337
+ # Task 1: Load state if we just started
338
+ if LAST_EVENT_ID is None:
339
+ load_state()
340
 
341
  new_events, err = check_new_events(LAST_EVENT_ID, xsrf, bip)
342
 
343
  if err:
344
+ logger.error(f"Error scraping events: {err}")
345
  send_email_message(
346
  "⚠️ BIP Scraper Error",
347
  "⚠️ <b>Scraper Error!</b><br><br>"
 
351
  is_html=True
352
  )
353
  SESSION_EXPIRED = True
 
 
354
  return
355
 
356
  if new_events:
 
358
  if LAST_EVENT_ID is None:
359
  LAST_EVENT_ID = new_events[0]["id"]
360
  LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
361
+ save_state(LAST_EVENT_ID)
362
+ logger.info(f"EVENT ID : {LAST_EVENT_CODE} (Tracking started)")
363
  else:
364
  send_event_alerts(new_events)
365
  LAST_EVENT_ID = new_events[0]["id"]
366
  LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
367
+ save_state(LAST_EVENT_ID)
368
+
369
  for ev in new_events:
370
  code = ev.get('event_code', ev['id'])
371
+ logger.info(f"🚨 NEW EVENT ID : {code} (Alert Sent!)")
372
  else:
373
+ logger.info(f"EVENT ID : {LAST_EVENT_CODE}")
 
374
 
375
  def list_all_events():
376
  """Fetches the first page of events from BIP and prints them."""
377
+ logger.info("Fetching recent events from BIP...")
378
 
379
  load_dotenv(override=True)
380
  xsrf = os.getenv("XSRF_TOKEN", "")
 
382
 
383
  data, err = fetch_bip_events(xsrf, bip, page=1)
384
  if err:
385
+ logger.error(f"Error: {err}")
386
  return
387
 
388
  resources = data.get("resources", [])
389
  if not resources:
390
+ logger.info("No events found.")
391
  return
392
 
393
+ logger.info(f"\nFound {len(resources)} recent events:")
394
  print("-" * 60)
395
  for res in resources:
396
  ev = parse_event(res)
 
399
 
400
  def get_latest_event():
401
  """Fetches and prints only the single most recent event."""
402
+ logger.info("Fetching the latest event...")
403
 
404
  load_dotenv(override=True)
405
  xsrf = os.getenv("XSRF_TOKEN", "")
 
407
 
408
  data, err = fetch_bip_events(xsrf, bip, page=1)
409
  if err:
410
+ logger.error(f"Error: {err}")
411
  return
412
 
413
  resources = data.get("resources", [])
414
  if not resources:
415
+ logger.info("No events found.")
416
  return
417
 
418
  ev = parse_event(resources[0])
 
429
 
430
  def test_email_alert():
431
  """Sends a dummy test message to the configured Email."""
432
+ logger.info("Sending test exact alert to Email...")
433
+ success = send_email_message("🤖 Test Alert", "🤖 <b>Test Alert from BIP CLI Notifier</b><br><br>Your currently configured email system is working perfectly!", is_html=True)
434
  if success:
435
+ logger.info("✅ Test message sent successfully!")
436
  else:
437
+ logger.error("❌ Failed to send test message. Check your .env configuration.")
438
 
439
  def start_loop():
440
+ logger.info("=" * 60)
441
+ logger.info("🚀 BIP CLI Notifier Started")
442
+ logger.info("=" * 60)
443
+ logger.info("Press Ctrl+C to stop the notifier at any time.\n")
444
 
445
  try:
446
  while True:
 
455
  if remaining > 0:
456
  time.sleep(min(5, remaining))
457
  except KeyboardInterrupt:
458
+ logger.info("\n🛑 Keyboard interrupt received.")
459
+ finally:
460
+ logger.info("Cleaning up resources...")
461
+ try:
462
+ SESSION.close()
463
+ except:
464
+ pass
465
+ logger.info("Notifier stopped gracefully. Goodbye!")
466
 
467
+ # ==============================================================================
468
+ # FASTAPI MICROSERVICE (Task 10)
469
+ # ==============================================================================
470
+ app = FastAPI(title="BIP Auto Notifier")
471
+
472
+ def background_scraper_loop():
473
+ """Runs the main notification loop within the FastAPI background."""
474
+ start_loop()
475
+
476
+ @app.on_event("startup")
477
+ async def startup_event():
478
+ logger.info("FastAPI starting up. Launching background tracker thread...")
479
+ threading.Thread(target=background_scraper_loop, daemon=True).start()
480
 
481
+ @app.get("/health")
482
+ async def health_check():
483
  """
484
+ Task 6 & 10: Improved Health Endpoint
485
+ Returns status, active tracking state, and expiration errors.
486
  """
487
+ return {
488
+ "status": "online",
489
+ "session_expired": SESSION_EXPIRED,
490
+ "last_event_id": LAST_EVENT_ID,
491
+ "timestamp": datetime.now().isoformat()
492
+ }
493
+
494
+ @app.get("/latest")
495
+ async def fetch_latest_api():
496
+ """Fetches the latest currently available event on the portal."""
497
+ load_dotenv(override=True)
498
+ xsrf = os.getenv("XSRF_TOKEN", "")
499
+ bip = os.getenv("BIP_SESSION", "")
500
 
501
+ data, err = fetch_bip_events(xsrf, bip, page=1)
502
+ if err:
503
+ return {"error": err}
504
+
505
+ resources = data.get("resources", [])
506
+ if not resources:
507
+ return {"message": "No events found."}
508
+
509
+ return parse_event(resources[0])
510
+
511
 
512
  if __name__ == "__main__":
513
+ logger.info("Starting BIP CLI Notifier...")
514
  parser = argparse.ArgumentParser(description="BIP Cloud Notifier CLI")
515
  parser.add_argument("--list-all", action="store_true", help="List all recent events and exit")
516
  parser.add_argument("--latest", action="store_true", help="Print details of the latest event and exit")
517
  parser.add_argument("--test-alert", action="store_true", help="Send a test message to Email and exit")
518
+ parser.add_argument("--run", action="store_true", help="Start the continuous monitoring loop (via FastAPI)")
519
+
520
+ # Task 8: Fix duplicate parsing
521
  args = parser.parse_args()
522
+ logger.debug(f"Parsed arguments: {args}")
523
 
524
  if args.list_all:
525
  list_all_events()
 
528
  elif args.test_alert:
529
  test_email_alert()
530
  elif args.run:
531
+ # Launch FastAPI which internally starts the loop
532
+ port = int(os.getenv("PORT", 7860))
533
+ uvicorn.run(app, host="0.0.0.0", port=port)
534
  else:
535
+ # Default behavior: run FastAPI
536
+ port = int(os.getenv("PORT", 7860))
537
+ uvicorn.run(app, host="0.0.0.0", port=port)