Conflicts resolved:
- browse.py: keep both --starts-before and --timezone args
- test_browse.py: combine TestStartsBeforeFilter and TestTimezoneParsing
- SKILL.md: combine documentation for both args
- Add RateLimiter class (token bucket algorithm)
- Thread-safe for use with ThreadPoolExecutor
- RATE_LIMIT_CALLS = 10 per RATE_LIMIT_WINDOW = 1 second
- _rate_limiter.acquire() called before each API request
- Also add MAX_RESPONSE_SIZE check in fetch_page
- Add sys import for stderr/exit
- Validate --detail index before accessing array
- Show error with available range instead of silent fallback to first event
- Exit with code 1 if --detail is out of range
- Add --starts-before CLI argument accepting Unix timestamp
- Filter match events to only show those starting before timestamp
- LIVE events are always included regardless of timestamp
- Update SKILL.md documentation
- Add TestStartsBeforeFilter with 3 unit tests
- Uses actual event count from page 1 to calculate total_pages
- Removes hardcoded '5' for events per page
- API changes to page size will be handled automatically
- Updated tests to match real API behavior (5 events per page)
- Fixed total_pages calculation: API returns 5 events/page, not PAGE_SIZE
- This was causing partial=false positives when max_total was used
- Updated tests to use correct pagination values
- Parallel page fetching with ThreadPoolExecutor (concurrency=5)
- File-based cache with 5 min TTL in ~/.cache/polymarket-browse/
- New --no-cache flag to bypass cache
- New --max-total parameter for early exit
- Updated tests to work with new implementation
- Added TypedDict classes for typed event/market structures
- Added type annotations to all functions
- Used Python 3.10+ union syntax (str | None, dict[str, Any])
- All 62 tests pass
New test classes:
- TestIsMatchMarket: 5 tests for is_match_market() classification
- TestGetMlMarket: 5 tests for get_ml_market() and get_ml_volume()
- TestFilterEvents: 5 tests for filter_events() and sort_events()
- TestFetchAllPages: 4 tests for fetch_all_pages() early-exit logic
- TestBrowseEvents: 5 tests for browse_events() sort_by parameter
Total: 24 new tests (62 total, all passing)
- fetch_page: replace subprocess.run(curl) with urllib (stdlib, cleaner)
- fetch_all_pages: add matches_max/non_matches_max params for early-exit.
When both are set, stop fetching once quotas are satisfied.
- browse_events: add sort_by param (None='fast' early-exit, 'volume'=full fetch+sort).
Early-exit only used when sort_by=None (no client-side sort needed).
- Remove subprocess import (no longer needed after migration)
Replace duplicate inline formatting with unified format+render pipeline.
New functions:
- format_match_event(e) — canonical dict for match events
- format_non_match_event(e) — canonical dict for non-match events
- render_match_lines(event_dict, i, mode) — text/HTML renderer
- render_non_match_lines(event_dict, i, mode) — text/HTML renderer
- send_chunked(...) — extracted Telegram chunking logic
Also fixed send_chunked() chunking bug: the original '. ' in line
check never matched event lines (period is followed by '</b>' not space).
Tests: 38 total, all passing.
Fixes: #14
Replace three duplicated time parsing functions with a single
_get_time_data(e, tz) helper returning {time_status, time_urgency, abs_time}.
Deleted functions:
- get_match_time_status(e) — urgency + status string
- get_match_time_str(e) — status string only
- get_start_time_wib(e) — (abs_time, rel_str) tuple
New unified helper:
- _get_time_data(e, tz=None) returns {time_status, time_urgency, abs_time}
- tz defaults to WIB (UTC+7, Indonesia)
- canonical rel_str format: 'LIVE', 'In 6h', '12h ago', etc.
- time_urgency: 0-3 (higher=livelier)
All call sites updated to use _get_time_data():
- format_event(), format_detail_event()
- print_browse(), print_detail()
- send_to_telegram()
Also: removed dead code in print_detail() that called get_match_time_str()
but never used the result.
Tests: 9 new tests for _get_time_data() covering TBD, future, live,
and past event scenarios. 19 tests total, all passing.
Fixes: #15
Add escape_html() function to prevent HTML injection in Telegram
parse_mode=HTML messages. Apply escaping to event titles inserted
into <a> tags in send_to_telegram().
- Add escape_html() using stdlib html.escape()
- Escape match event titles (line 648) and non-match titles (line 676)
- Add TestHtmlInjection with 2 tests proving fix:
- <script> tags escaped as <script>
- & ampersands escaped as &
- Fixes HIGH severity: titles from Polymarket API were inserted
without escaping, allowing malformed HTML in Telegram messages
Extract the nested send() function into a module-level
send_telegram_message(bot_token, chat_id, text, timeout=10)
function. This enables unit testing without hitting the real
Telegram API.
Changes:
- Add send_telegram_message() at module level in TELEGRAM section
- Replace nested send() with thin wrapper that calls
send_telegram_message()
- Update argparse --telegram help text to use TELEGRAM_BOT_TOKEN
- Add tests/test_browse.py with 8 unit tests covering:
- Success case (returns message_id)
- API error (RuntimeError)
- Invalid token (HTTPError 404)
- Rate limit (HTTPError 429)
- Network error (URLError)
- Timeout (URLError)
- Custom timeout parameter
- HTML parse_mode in request
Ref: #4
Replace curl subprocess with urllib.request to prevent token leakage via
ps aux / /proc/*/cmdline. Token now stays in process memory only.
Changes:
- Remove subprocess import, add urllib.parse.urlencode and urllib.request
- Replace curl subprocess call with urlopen(Request(...))
- Change env var BOT_TOKEN -> TELEGRAM_BOT_TOKEN (clearer naming)
- Raise RuntimeError on missing env vars, API errors, or network errors
- Add 10s timeout to urlopen
Fixes#4