refactor: deep review fixes round 3

- client.py:
  - Remove dead _extract_cursor second branch (unreachable code)
  - Cache SSL context as module-level _SSL_CTX (avoid re-reading CA certs)
  - Add 404 stale-fallback retry to _graphql_post (parity with GET)
  - Remove dead core.get('name')/core.get('screen_name') in fetch_user
  - Set Content-Type: application/json only for POST requests
  - Rename _to_int → _parse_int for clarity vs config._as_int
  - Add 'not thread-safe' note on module-level caches

- cli.py:
  - _fetch_and_display now accepts optional config param (fix double load)
  - Refactor user_posts to use _fetch_and_display
  - Pass config to all _fetch_and_display callers

- pyproject.toml:
  - Move xclienttransaction/requests to optional [transaction] deps
  - Add beautifulsoup4 to [transaction] optional deps

- README.md:
  - Add rateLimit config section with comments
  - Add constants.py to project structure tree
This commit is contained in:
jackwener
2026-03-08 13:58:06 +08:00
parent 625181b76c
commit d2d971c865
5 changed files with 79 additions and 56 deletions

View File

@@ -115,10 +115,11 @@ def cli(verbose):
_setup_logging(verbose)
def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, output_file, do_filter):
# type: (Any, str, str, Optional[int], bool, Optional[str], bool) -> None
def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, output_file, do_filter, config=None):
# type: (Any, str, str, Optional[int], bool, Optional[str], bool, Optional[dict]) -> None
"""Common fetch-filter-display logic for timeline-like commands."""
config = load_config()
if config is None:
config = load_config()
try:
fetch_count = _resolve_fetch_count(max_count, config.get("fetch", {}).get("count", 50))
console.print("%s Fetching %s (%d tweets)...\n" % (emoji, label, fetch_count))
@@ -211,7 +212,7 @@ def favorite(max_count, as_json, output_file, do_filter):
client = _get_client(config)
_fetch_and_display(
lambda count: client.fetch_bookmarks(count),
"favorites", "🔖", max_count, as_json, output_file, do_filter,
"favorites", "🔖", max_count, as_json, output_file, do_filter, config,
)
@@ -243,26 +244,17 @@ def user_posts(screen_name, max_count, as_json):
"""List a user's tweets. SCREEN_NAME is the @handle (without @)."""
screen_name = screen_name.lstrip("@")
config = load_config()
client = _get_client(config)
console.print("👤 Fetching @%s's profile..." % screen_name)
try:
fetch_count = _resolve_fetch_count(max_count, 20)
client = _get_client(config)
console.print("👤 Fetching @%s's profile..." % screen_name)
profile = client.fetch_user(screen_name)
console.print("📝 Fetching tweets (%d)...\n" % fetch_count)
start = time.time()
tweets = client.fetch_user_tweets(profile.id, fetch_count)
elapsed = time.time() - start
console.print("✅ Fetched %d tweets in %.1fs\n" % (len(tweets), elapsed))
except RuntimeError as exc:
console.print("[red]❌ %s[/red]" % exc)
sys.exit(1)
if as_json:
click.echo(tweets_to_json(tweets))
return
print_tweet_table(tweets, console, title="📝 @%s%d tweets" % (screen_name, len(tweets)))
console.print()
_fetch_and_display(
lambda count: client.fetch_user_tweets(profile.id, count),
"@%s tweets" % screen_name, "📝", max_count, as_json, None, False, config,
)
SEARCH_PRODUCTS = ["Top", "Latest", "Photos", "Videos"]
@@ -288,7 +280,7 @@ def search(query, product, max_count, as_json, do_filter):
client = _get_client(config)
_fetch_and_display(
lambda count: client.fetch_search(query, count, product),
"'%s' (%s)" % (query, product), "🔍", max_count, as_json, None, do_filter,
"'%s' (%s)" % (query, product), "🔍", max_count, as_json, None, do_filter, config,
)
@@ -307,7 +299,7 @@ def likes(screen_name, max_count, as_json, do_filter):
profile = client.fetch_user(screen_name)
_fetch_and_display(
lambda count: client.fetch_user_likes(profile.id, count),
"@%s likes" % screen_name, "❤️", max_count, as_json, None, do_filter,
"@%s likes" % screen_name, "❤️", max_count, as_json, None, do_filter, config,
)
@@ -356,7 +348,7 @@ def list_timeline(list_id, max_count, as_json, do_filter):
client = _get_client(config)
_fetch_and_display(
lambda count: client.fetch_list_timeline(list_id, count),
"list %s" % list_id, "📋", max_count, as_json, None, do_filter,
"list %s" % list_id, "📋", max_count, as_json, None, do_filter, config,
)