add agent workspace

This commit is contained in:
2026-02-18 22:39:35 -08:00
commit e8389b8772
48 changed files with 3476 additions and 0 deletions

View 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())