add agent workspace
This commit is contained in:
193
scripts/ucla_pilates_monitor.py
Normal file
193
scripts/ucla_pilates_monitor.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UCLA Reformer Pilates Course Monitor - Date-aware Version
|
||||
Only reports courses that are NOT "Full" AND not yet started/expired
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import datetime
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
# Course URLs to monitor
|
||||
COURSES = {
|
||||
"Reformer Pilates (Enrolled)": "https://secure.recreation.ucla.edu/Program/GetProgramDetails?courseId=d7adf66a-d3a6-46d6-96c7-54e4c015dcf1",
|
||||
"Reformer Pilates (Standby)": "https://secure.recreation.ucla.edu/Program/GetProgramDetails?courseId=7abbf877-f1cf-4ddc-a0ef-690ff935b39a"
|
||||
}
|
||||
|
||||
# Sections to exclude (time doesn't work for us)
|
||||
EXCLUDE_SECTIONS = [
|
||||
"Sec 16B", # Wednesday 12:00pm - not available
|
||||
"Sec 19B", # Friday 12:00pm - not available
|
||||
]
|
||||
|
||||
def should_exclude(text):
|
||||
"""Check if course should be excluded based on section/time"""
|
||||
for exclude in EXCLUDE_SECTIONS:
|
||||
if exclude in text:
|
||||
return True
|
||||
return False
|
||||
|
||||
def parse_date_range(text):
|
||||
"""Extract date range from course text like (1/5-2/6) or (2/13-3/13)"""
|
||||
# Match patterns like (1/5-2/6) or (2/13-3/13)
|
||||
match = re.search(r'\((\d{1,2})/(\d{1,2})-(\d{1,2})/(\d{1,2})\)', text)
|
||||
if match:
|
||||
start_month, start_day, end_month, end_day = match.groups()
|
||||
current_year = datetime.now().year
|
||||
try:
|
||||
start_date = datetime(current_year, int(start_month), int(start_day))
|
||||
end_date = datetime(current_year, int(end_month), int(end_day))
|
||||
return start_date, end_date
|
||||
except ValueError:
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
def is_course_active(start_date, end_date):
|
||||
"""Check if course is still active (not yet ended)"""
|
||||
if not end_date:
|
||||
return True # Can't parse date, assume active
|
||||
today = datetime.now()
|
||||
# Course is active if it hasn't ended yet (give 1 day buffer)
|
||||
return end_date >= today
|
||||
|
||||
def is_valid_course_entry(text):
|
||||
"""Check if text is a valid course entry (not description/no-offering text)"""
|
||||
text_lower = text.lower()
|
||||
|
||||
# Exclude these patterns
|
||||
exclude_patterns = [
|
||||
"there are no offerings available",
|
||||
"to view the class times",
|
||||
"please visit the",
|
||||
"this standby pass is valid",
|
||||
"instructor:",
|
||||
"reformer pilates - standby pass", # Header text
|
||||
"×", # Close button
|
||||
]
|
||||
|
||||
for pattern in exclude_patterns:
|
||||
if pattern in text_lower:
|
||||
return False
|
||||
|
||||
# Must contain course identifier (Sec X or Session)
|
||||
has_course_id = bool(re.search(r'(Sec \d+[A-Z]|Session [A-Z])', text))
|
||||
|
||||
# Must contain price or day/time info
|
||||
has_info = bool(re.search(r'(\$\d+|[MTWTF]{1,2},? \d{1,2}:\d{2})', text))
|
||||
|
||||
return has_course_id and has_info
|
||||
|
||||
async def check_course(page, name, url):
|
||||
"""Check a single course page, return available sections"""
|
||||
available = []
|
||||
|
||||
try:
|
||||
await page.goto(url, wait_until="networkidle", timeout=30000)
|
||||
await page.wait_for_selector("text=Offerings", timeout=10000)
|
||||
|
||||
# Get all semester tabs
|
||||
semesters = await page.query_selector_all("[role='tab']")
|
||||
|
||||
for semester in semesters:
|
||||
sem_name = await semester.inner_text()
|
||||
sem_name = sem_name.strip()
|
||||
|
||||
await semester.click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
# Find all course sections
|
||||
sections = await page.query_selector_all(".offering-item, [class*='offering'], .card, .list-group-item, tr")
|
||||
|
||||
for section in sections:
|
||||
try:
|
||||
text = await section.inner_text()
|
||||
if not text or len(text) < 30:
|
||||
continue
|
||||
|
||||
text_lower = text.lower()
|
||||
|
||||
# Check if it's NOT full
|
||||
is_full = "full" in text_lower
|
||||
if is_full:
|
||||
continue
|
||||
|
||||
# Check if it's a valid course entry
|
||||
if not is_valid_course_entry(text):
|
||||
continue
|
||||
|
||||
# Check if excluded (time doesn't work)
|
||||
if should_exclude(text):
|
||||
continue
|
||||
|
||||
# Check date range
|
||||
start_date, end_date = parse_date_range(text)
|
||||
if not is_course_active(start_date, end_date):
|
||||
continue # Course has ended
|
||||
|
||||
# Extract clean info
|
||||
# Remove extra whitespace and truncate
|
||||
lines = [line.strip() for line in text.strip().split('\n') if line.strip()]
|
||||
info = ' | '.join(lines[:3]) # First 3 lines max
|
||||
info = info[:200] # Limit length
|
||||
|
||||
# Format dates nicely
|
||||
if start_date and end_date:
|
||||
date_str = f"{start_date.strftime('%m/%d')}-{end_date.strftime('%m/%d')}"
|
||||
else:
|
||||
date_str = ""
|
||||
|
||||
available.append({
|
||||
'semester': sem_name,
|
||||
'info': info,
|
||||
'dates': date_str,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date
|
||||
})
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
return [{'error': f"Error checking {name}: {e}"}]
|
||||
|
||||
return available
|
||||
|
||||
async def main():
|
||||
"""Main function - only output available and active courses"""
|
||||
all_available = []
|
||||
today_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
await page.set_viewport_size({"width": 1280, "height": 800})
|
||||
|
||||
for name, url in COURSES.items():
|
||||
available = await check_course(page, name, url)
|
||||
if available and not any('error' in str(item) for item in available):
|
||||
all_available.append((name, available))
|
||||
|
||||
await browser.close()
|
||||
|
||||
# Only print if there are available courses
|
||||
if all_available:
|
||||
print(f"🚨 UCLA Pilates - Available Courses ({today_str})")
|
||||
print("=" * 60)
|
||||
|
||||
for name, courses in all_available:
|
||||
print(f"\n📋 {name}:")
|
||||
for course in courses:
|
||||
# Format: [Winter 2026] 📅 02/11-03/11
|
||||
date_str = f"📅 {course['dates']}" if course['dates'] else ""
|
||||
print(f" ✅ [{course['semester']}] {date_str}")
|
||||
print(f" {course['info']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("👉 Enroll at: https://secure.recreation.ucla.edu")
|
||||
else:
|
||||
# No available courses - silent
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user