Skip to content

Commit eb259e8

Browse files
committed
client: refactor url handling
1 parent 3fd2e6e commit eb259e8

5 files changed

Lines changed: 48 additions & 46 deletions

File tree

client/livecli/commands/scrape.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,28 +44,27 @@ def _extract_filename_from_url(url: str) -> str:
4444
return components[-1] if components else 'resource'
4545

4646

47-
def _load_test_resources(url_list: list[str]) -> dict[str, bytes]:
47+
def _load_test_resources(url_dict: dict[str, str]) -> dict[str, bytes]:
4848
"""Load test resources from local files.
4949
5050
Args:
51-
url_list: List of local file paths from scraper.get_urls()
51+
url_dict: Dict mapping resource key to local file path from scraper.get_urls()
5252
5353
Returns:
54-
Dict mapping URL to bytes
54+
Dict mapping resource key to bytes
5555
"""
5656
resources = {}
5757

58-
for url in url_list:
58+
for key, url in url_dict.items():
5959
# Strip query parameters (e.g., ?public=true) from file path
60-
# but keep original URL as key in resources dict
6160
base_path = url.split('?')[0] if '?' in url else url
6261

6362
# Try common extensions
6463
for ext in ['', '.json', '.html', '.txt']:
6564
filepath = f'{base_path}{ext}'
6665
if os.path.exists(filepath):
6766
with open(filepath, 'rb') as f:
68-
resources[url] = f.read()
67+
resources[key] = f.read()
6968
break
7069
else:
7170
raise FileNotFoundError(f'File not found: {base_path} (tried extensions: .json, .html, .txt, and no extension)')
@@ -87,37 +86,40 @@ def _wait_next_tick(interval_seconds: int) -> None:
8786
time.sleep(next_tick - now)
8887

8988

90-
def _fetch_resources(session: requests.Session, url_list: list[str], timeout: float) -> dict[str, bytes]:
89+
def _fetch_resources(session: requests.Session, url_dict: dict[str, str], timeout: float) -> dict[str, bytes]:
9190
"""Fetch all resources from URLs.
9291
9392
Args:
9493
session: requests.Session to use
95-
url_list: List of URLs to fetch
94+
url_dict: Dict mapping resource key to URL
9695
timeout: Request timeout in seconds
9796
9897
Returns:
99-
Dict mapping URL to response bytes
98+
Dict mapping resource key to response bytes
10099
"""
101100
resources = {}
102-
for url in url_list:
101+
for key, url in url_dict.items():
103102
r = session.get(url, timeout=timeout)
104103
r.raise_for_status()
105-
resources[url] = r.content
104+
resources[key] = r.content
106105
return resources
107106

108107

109-
def _save_resources(resources: dict[str, bytes], log_dir: str, timestamp: int) -> None:
108+
def _save_resources(resources: dict[str, bytes], url_dict: dict[str, str], log_dir: str, timestamp: int) -> None:
110109
"""Save fetched resources to archive files.
111110
112111
Args:
113-
resources: Dict mapping URL to bytes
112+
resources: Dict mapping resource key to bytes
113+
url_dict: Dict mapping resource key to URL (for determining file extension)
114114
log_dir: Directory to save files
115115
timestamp: Unix timestamp for filenames
116116
"""
117-
for url, content in resources.items():
118-
filename_base = _extract_filename_from_url(url)
117+
for key, content in resources.items():
118+
# Use the resource key as filename base (e.g., 'problems', 'scoreboard', 'teams')
119+
filename_base = key
119120

120121
# Determine file extension from URL
122+
url = url_dict[key]
121123
if '/api/' in url or 'json' in url.lower():
122124
ext = '.json'
123125
else:
@@ -158,7 +160,7 @@ def scrape_main(options: argparse.Namespace) -> None:
158160
last_standings = init_feeds[types.FeedType.STANDINGS]
159161

160162
session = requests.Session()
161-
url_list = scraper.get_urls(scoreboard_url)
163+
url_dict = scraper.get_urls(scoreboard_url)
162164

163165
# Pre-configure authentication if credentials are available
164166
if scraper.has_credentials():
@@ -167,7 +169,7 @@ def scrape_main(options: argparse.Namespace) -> None:
167169

168170
logging.info('Attempting an initial scrape...')
169171
try:
170-
resources = _fetch_resources(session, url_list, options.interval_seconds * 0.9)
172+
resources = _fetch_resources(session, url_dict, options.interval_seconds * 0.9)
171173
scraper.scrape(resources)
172174
except Exception:
173175
logging.exception('Unhandled exception')
@@ -198,8 +200,8 @@ def scrape_main(options: argparse.Namespace) -> None:
198200
try:
199201
logging.info('Scraping...%s' % ('' if options.upload and upload else ' (dry-run)'))
200202
timestamp = int(time.time())
201-
resources = _fetch_resources(session, url_list, options.interval_seconds * 0.9)
202-
_save_resources(resources, log_dir, timestamp)
203+
resources = _fetch_resources(session, url_dict, options.interval_seconds * 0.9)
204+
_save_resources(resources, url_dict, log_dir, timestamp)
203205

204206
try:
205207
standings = scraper.scrape(resources)

client/livecli/scrapers/base.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,25 @@ class NeedLoginException(Exception):
4848

4949

5050
class Scraper(abc.ABC):
51-
def get_urls(self, base_url: str) -> list[str]:
52-
"""Return list of URLs to fetch.
51+
def get_urls(self, base_url: str) -> dict[str, str]:
52+
"""Return dict of resource keys to URLs to fetch.
5353
54-
Default implementation returns single URL (base_url).
54+
Default implementation returns single URL with key 'standings'.
5555
Override for scrapers that need multiple URLs.
5656
5757
Args:
5858
base_url: Base URL from --scoreboard-url
5959
6060
Returns:
61-
List of URLs to fetch
61+
Dict mapping resource keys to URLs to fetch
6262
"""
63-
return [base_url]
63+
return {'standings': base_url}
6464

6565
def scrape(self, resources: dict[str, bytes]) -> dict[str, Any]:
6666
"""Parse resources into standings format.
6767
6868
Args:
69-
resources: Dict mapping URL to raw bytes
69+
resources: Dict mapping resource key to raw bytes
7070
"""
7171
standings = self.scrape_impl(resources)
7272
if not standings['problems']:
@@ -80,7 +80,7 @@ def scrape_impl(self, resources: dict[str, bytes]) -> dict[str, Any]:
8080
"""Parse resources into standings.
8181
8282
Args:
83-
resources: Dict mapping URL to raw response bytes
83+
resources: Dict mapping resource key to raw response bytes
8484
8585
Returns:
8686
Dict with 'problems' and 'entries' keys

client/livecli/scrapers/domestic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def has_credentials(self) -> bool:
7171
self._options.login_user != '')
7272

7373
def scrape_impl(self, resources: dict[str, bytes]) -> dict[str, Any]:
74-
html = next(iter(resources.values())).decode('utf-8')
74+
html = resources['standings'].decode('utf-8')
7575
standings = {'problems': [], 'entries': []}
7676
if 'rehearsal' in html and not self._options.allow_rehearsal:
7777
logging.info('Contest has not started yet.')

client/livecli/scrapers/domjudge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def has_credentials(self) -> bool:
4949
self._options.login_user != '')
5050

5151
def scrape_impl(self, resources: dict[str, bytes]) -> dict[str, Any]:
52-
html = next(iter(resources.values())).decode('utf-8')
52+
html = resources['standings'].decode('utf-8')
5353
doc = bs4.BeautifulSoup(html, 'html5lib')
5454

5555
if doc.select('#loginform'):

client/livecli/scrapers/domjudge_api.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,38 +40,38 @@ class DomjudgeApiScraper(base.Scraper):
4040
def __init__(self, options: argparse.Namespace):
4141
self._options = options
4242

43-
def get_urls(self, base_url: str) -> list[str]:
44-
"""Declare the three API endpoints needed."""
43+
def get_urls(self, base_url: str) -> dict[str, str]:
44+
"""Declare the three API endpoints needed.
45+
46+
Returns:
47+
Dict mapping resource keys to URLs:
48+
- 'problems': problems endpoint
49+
- 'scoreboard': scoreboard endpoint (with ?public=true if --public flag set)
50+
- 'teams': teams endpoint
51+
"""
4552
# Add ?public=true to scoreboard URL if --public flag is set
4653
scoreboard_url = f'{base_url}/scoreboard'
4754
if hasattr(self._options, 'public') and self._options.public:
4855
scoreboard_url += '?public=true'
4956

50-
return [
51-
f'{base_url}/problems',
52-
scoreboard_url,
53-
f'{base_url}/teams',
54-
]
57+
return {
58+
'problems': f'{base_url}/problems',
59+
'scoreboard': scoreboard_url,
60+
'teams': f'{base_url}/teams',
61+
}
5562

5663
def scrape_impl(self, resources: dict[str, bytes]) -> dict[str, Any]:
5764
"""Scrape DOMjudge contest data using the REST API.
5865
5966
Args:
60-
resources: Dict mapping URL to raw bytes
67+
resources: Dict mapping resource key to raw bytes
6168
6269
Returns:
6370
Dictionary with 'problems' and 'entries' keys in LiveSite format
6471
"""
65-
from urllib.parse import urlparse
66-
67-
# Match URLs by path component (ignoring query parameters like ?public=true)
68-
problems_url = next(url for url in resources.keys() if urlparse(url).path.endswith('/problems'))
69-
scoreboard_url = next(url for url in resources.keys() if urlparse(url).path.endswith('/scoreboard'))
70-
teams_url = next(url for url in resources.keys() if urlparse(url).path.endswith('/teams'))
71-
72-
problems_data = json.loads(resources[problems_url])
73-
scoreboard_data = json.loads(resources[scoreboard_url])
74-
teams_data = json.loads(resources[teams_url])
72+
problems_data = json.loads(resources['problems'])
73+
scoreboard_data = json.loads(resources['scoreboard'])
74+
teams_data = json.loads(resources['teams'])
7575

7676
team_id_to_name = {team['id']: team['name'] for team in teams_data}
7777

0 commit comments

Comments
 (0)