feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources
v3 rewrites the search engine from the ground up: - Intelligent pre-research: resolves X handles, GitHub repos, subreddits, TikTok hashtags, and YouTube channels before searching - GitHub person-mode: PR velocity, top repos by stars, release notes - GitHub project-mode: live star counts, README, releases, top issues - ELI5 mode: plain language synthesis, no jargon - 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, Threads, Pinterest, Perplexity, Bluesky, Web - Free Reddit comments via public JSON (no API key needed) - Fun judge v2: humor scoring baked into narrative - Cookie consent before browser scanning - 10,000 free ScrapeCreators calls - 1,012 tests Thank you to the community contributors whose issues and PRs shaped v3: @uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116), @thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78), @zl190 (#115), @hnshah (#84, #85, #86) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
-12
@@ -1,26 +1,28 @@
|
||||
"""HTTP utilities for last30days skill (stdlib only)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from . import log as _log
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
DEBUG = os.environ.get("LAST30DAYS_DEBUG", "").lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
"""Log debug message to stderr."""
|
||||
if DEBUG:
|
||||
sys.stderr.write(f"[DEBUG] {msg}\n")
|
||||
sys.stderr.flush()
|
||||
_log.debug(msg)
|
||||
|
||||
|
||||
MAX_RETRIES = 5
|
||||
MAX_429_RETRIES = 2
|
||||
RETRY_DELAY = 2.0
|
||||
USER_AGENT = "last30days-skill/2.1 (Assistant Skill)"
|
||||
USER_AGENT = "last30days-skill/3.0 (Assistant Skill)"
|
||||
|
||||
|
||||
class HTTPError(Exception):
|
||||
@@ -38,8 +40,9 @@ def request(
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
retries: int = MAX_RETRIES,
|
||||
max_429_retries: int = MAX_429_RETRIES,
|
||||
raw: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
) -> Union[Dict[str, Any], str]:
|
||||
"""Make an HTTP request and return JSON response.
|
||||
|
||||
Args:
|
||||
@@ -49,9 +52,11 @@ def request(
|
||||
json_data: Optional JSON body (for POST)
|
||||
timeout: Request timeout in seconds
|
||||
retries: Number of retries on failure
|
||||
max_429_retries: Maximum 429 retries before giving up (separate cap)
|
||||
raw: If True, return raw response text instead of parsed JSON
|
||||
|
||||
Returns:
|
||||
Parsed JSON response (or raw text if raw=True)
|
||||
Parsed JSON response as dict, or raw text string if raw=True.
|
||||
|
||||
Raises:
|
||||
HTTPError: On request failure
|
||||
@@ -66,9 +71,11 @@ def request(
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
|
||||
log(f"{method} {url}")
|
||||
safe_url = re.sub(r'([?&])(key|api_key|token|secret)=[^&]*', r'\1\2=***', url)
|
||||
log(f"{method} {safe_url}")
|
||||
|
||||
last_error = None
|
||||
rate_limit_count = 0
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
@@ -81,7 +88,7 @@ def request(
|
||||
body = None
|
||||
try:
|
||||
body = e.read().decode('utf-8')
|
||||
except:
|
||||
except (OSError, UnicodeDecodeError):
|
||||
pass
|
||||
log(f"HTTP Error {e.code}: {e.reason}")
|
||||
if body:
|
||||
@@ -93,6 +100,12 @@ def request(
|
||||
if 400 <= e.code < 500 and e.code != 429:
|
||||
raise last_error
|
||||
|
||||
# Cap 429 retries separately to avoid wasting latency
|
||||
if e.code == 429:
|
||||
rate_limit_count += 1
|
||||
if rate_limit_count >= max_429_retries:
|
||||
raise last_error
|
||||
|
||||
if attempt < retries - 1:
|
||||
if e.code == 429:
|
||||
# Respect Retry-After header, fall back to exponential backoff
|
||||
@@ -103,7 +116,7 @@ def request(
|
||||
except ValueError:
|
||||
delay = RETRY_DELAY * (2 ** attempt) + 1
|
||||
else:
|
||||
delay = RETRY_DELAY * (2 ** attempt) + 1 # 2s, 5s, 9s...
|
||||
delay = RETRY_DELAY * (2 ** attempt) + 1 # 3s, 5s, 9s...
|
||||
log(f"Rate limited (429). Waiting {delay:.1f}s before retry {attempt + 2}/{retries}")
|
||||
else:
|
||||
delay = RETRY_DELAY * (2 ** attempt)
|
||||
|
||||
Reference in New Issue
Block a user