Skip to content

fix: consolidate price population scripts, remove broken Steam WebAPI#14

Merged
SuaveIV merged 7 commits intomainfrom
fix/consolidate-price-scripts
Mar 27, 2026
Merged

fix: consolidate price population scripts, remove broken Steam WebAPI#14
SuaveIV merged 7 commits intomainfrom
fix/consolidate-price-scripts

Conversation

@SuaveIV
Copy link
Copy Markdown
Owner

@SuaveIV SuaveIV commented Mar 27, 2026

What

Consolidated three duplicate price population scripts into one working script.

Why

The original populate_prices.py was crashing with:

AttributeError: 'WebAPIInterface' object has no attribute 'GetAppList'

Turns out all three scripts (populate_prices.py, populate_prices_optimized.py, populate_prices_async.py) had the same broken code. The GetAppList method doesn't exist in the Steam WebAPI library - we verified against the actual docs.

What changed

  • Deleted populate_prices.py (original, broken)
  • Deleted populate_prices_optimized.py (ThreadPoolExecutor version, same bug)
  • Renamed populate_prices_async.py -> populate_prices.py (cleaned up)
  • Removed steam.webapi dependency entirely
  • The Steam WebAPI fallback wasn't even useful - it only grabbed game names without price data

What stays the same

  • Steam Store API (/api/appdetails) works fine for prices
  • ITAD API endpoints are correct: /games/lookup/v1 and /games/prices/v3
  • Async architecture with adaptive rate limiting
  • Batch database writes for safety

Testing

Run with --dry-run first to verify it works:

uv run scripts/populate_prices.py --dry-run

or

just populate-prices --dry-run

Summary by CodeRabbit

  • New Features

    • High-concurrency, async price fetching for much faster updates
    • Adaptive rate-limit mode for smarter throttling
  • Improvements

    • Two-phase fetch-and-batch persist pipeline with transactional batch commits and per-record salvage
    • More reliable retry/backoff behavior and ensured HTTP client cleanup on exit
    • CLI validation and clearer concurrency/rate-limit flags
  • Removed

    • Legacy fast/turbo execution recipes and duplicate population scripts

SuaveIV added 2 commits March 27, 2026 15:49
- Removed broken Steam WebAPI fallback (GetAppList doesn't exist)
- Removed steam.webapi dependency entirely
- Based on async version with proper ITAD API integration
- Uses /games/lookup/v1 and /games/prices/v3 endpoints correctly
- Adaptive rate limiting with configurable concurrency
- Batch database writes for safety
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Warning

Rate limit exceeded

@SuaveIV has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 24 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 24 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7dd07beb-381b-4566-aea1-b768e7734da8

📥 Commits

Reviewing files that changed from the base of the PR and between 7a28895 and 8d4ee11.

📒 Files selected for processing (1)
  • scripts/populate_prices.py
📝 Walkthrough

Walkthrough

The pull request consolidates price-fetching by converting scripts/populate_prices.py to an async, high-concurrency pipeline with adaptive rate limiting and batched transactional writes, removes two legacy implementations (scripts/populate_prices_async.py, scripts/populate_prices_optimized.py), and updates the CLI and justfile recipes.

Changes

Cohort / File(s) Summary
Main Script (refactor & async)
scripts/populate_prices.py
Rewritten as async: adds PricePopulator.aclose, semaphore-based concurrency (max_concurrent), adaptive per-API rate limiting, async retry/backoff, per-game async fetchs (fetch_steam_price_single, fetch_itad_price_single), two-phase flow (concurrent collection + batch_write_* transactional batched writes with per-record salvage), CLI flags changed (--concurrent, --rate-limit), main()async def and run via asyncio.run.
Removed legacy scripts
scripts/populate_prices_async.py, scripts/populate_prices_optimized.py
Deleted alternative async/optimized implementations that previously contained separate populator classes, adaptive throttling, batch DB writes, and CLI entrypoints; functionality consolidated into populate_prices.py.
Build/recipes
justfile
Removed populate-prices-fast and populate-prices-turbo recipes; updated populate-prices message to reflect consolidated behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Main as Main (Orchestrator)
    participant Sem as Semaphore (Concurrency Control)
    participant API as Steam/ITAD APIs
    participant DB as Database (Batch Writer)

    rect rgba(100,150,200,0.5)
    Note over Main,API: Phase 1 — Concurrent collection
    Main->>Main: load game IDs
    Main->>Sem: init (max_concurrent)
    loop per game (asyncio.as_completed)
        Main->>Sem: acquire
        Main->>API: fetch_steam_price_single(app_id) / fetch_itad_price_single(app_id)
        API-->>Main: (app_id, success, payload, error)
        Main->>Sem: release
    end
    end

    rect rgba(150,200,100,0.5)
    Note over Main,DB: Phase 2 — Batched writes
    Main->>DB: batch_write_steam_data(steam_data, batch_size)
    alt Transaction succeeds
        DB->>DB: commit batch
        DB-->>Main: rows_written
    else Batch fails
        DB->>DB: rollback
        loop per-record salvage
            DB->>DB: write single record
        end
        DB-->>Main: partial_rows_written
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code, async and spry—
Semaphores open, old sleeps goodbye.
Batches land tidy in the warren of rows,
Adaptive pace where the green signal glows.
Hooray, one script now leads all the shows! 🌱

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: consolidating three duplicate price population scripts into one working implementation and removing the broken Steam WebAPI dependency.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/consolidate-price-scripts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/populate_prices.py`:
- Around line 71-90: The rate limiter dictionary keys (current_delays,
min_delays, max_delays, error_counts, success_counts, last_adjustment,
rate_limit_locks, last_request_times) use "store_api"/"itad_api" but the rest of
the code and request calls pass "store"/"itad", causing KeyError; fix by
normalizing the keys to match the request paths (use "store" and "itad") or
vice‑versa — update the initializer dictionaries and any references inside
rate_limited_request and adaptive_rate_limit to use the same keys
("store"/"itad"), and also update related uses in the fetcher/error handling
code noted (around the mentioned ranges) so all accesses reference the unified
symbols.
- Around line 192-209: The code currently treats any non-429 response as
success; change the logic in the response handling block so only 2xx responses
are considered successful: check response.status_code in the 200-299 range, call
self.adaptive_rate_limit(api_type, True) and return the response only for 2xx;
for non-2xx (and non-429) responses increment retry handling the same as 429
(use backoff, logger warning/info, await asyncio.sleep, call
self.adaptive_rate_limit(api_type, False), and continue/retry up to
max_retries), and finally log and return None after max_retries; update the same
logic in both places called out (around the shown block and the 225-244 region)
and ensure handle_api_response receives None only when retries are exhausted.
- Around line 404-460: batch_write_steam_data is opening raw connections with
get_db_connection() and manually managing BEGIN/COMMIT/ROLLBACK, which bypasses
the process-level write-lock; replace those raw connections with the
write-locking context returned by get_write_connection() (use "with
get_write_connection() as conn:"), remove manual BEGIN/COMMIT/ROLLBACK calls
(let the context manage the lock/transaction) and ensure cache_game_details(...)
is called using that conn; apply the same change to the sibling function in the
462-538 range so both use get_write_connection() and proper context-scoped error
handling (on exceptions let the context handle rollback, and if you must retry
individual records, open a new get_write_connection() context for each retry and
avoid reusing the original raw conn).
- Around line 204-205: The code is building ITAD request URLs with inline query
strings (e.g., title={game_name} and the ITAD_API_KEY) and logging the full URL
(logger.warning("Max retries exceeded for %s", url)), which can leak secrets;
refactor the HTTP requests to pass query parameters as a separate params dict
(or use urllib.parse.urlencode) rather than concatenating them into the base
URL, ensure calls that build the URL variable do not include ITAD_API_KEY
inline, and change log calls (e.g., logger.warning and any retry logging around
self.adaptive_rate_limit) to log only the sanitized base URL and non-sensitive,
properly encoded parameter keys/values (or mask/hide API key) — apply the same
fix to the other occurrences in the file mentioned (lines ~291-312 and
~348-367).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f95d0b16-6551-4b9f-980e-e65ff073db1b

📥 Commits

Reviewing files that changed from the base of the PR and between c679fc8 and cf89e09.

📒 Files selected for processing (3)
  • scripts/populate_prices.py
  • scripts/populate_prices_async.py
  • scripts/populate_prices_optimized.py
💤 Files with no reviewable changes (2)
  • scripts/populate_prices_optimized.py
  • scripts/populate_prices_async.py

Comment thread scripts/populate_prices.py Outdated
Comment thread scripts/populate_prices.py Outdated
Comment thread scripts/populate_prices.py Outdated
Comment thread scripts/populate_prices.py
@SuaveIV SuaveIV self-assigned this Mar 27, 2026
- Update database connection import to use get_write_connection for clarity
- Simplify adaptive rate limiting dictionary keys by removing _api suffix
- Add params parameter to make_request_with_retry for query string support
- Extend retry logic to handle any non-2xx HTTP responses, not just 429
- Improve logging to include HTTP status codes and cleaned URLs
- Refactor Steam API request construction to use params dictionary
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/populate_prices.py (1)

734-749: ⚠️ Potential issue | 🟡 Minor

Reject contradictory CLI mode combinations.

--steam-only --itad-only currently exits successfully after doing nothing, and --refresh-current --itad-only prints a refresh banner even though the Steam path is skipped. These combinations should be rejected up front.

🛠️ Minimal fix
-    parser.add_argument(
-        "--steam-only", action="store_true", help="Only populate Steam Store prices"
-    )
-    parser.add_argument(
-        "--itad-only", action="store_true", help="Only populate ITAD historical prices"
-    )
+    target_group = parser.add_mutually_exclusive_group()
+    target_group.add_argument(
+        "--steam-only", action="store_true", help="Only populate Steam Store prices"
+    )
+    target_group.add_argument(
+        "--itad-only", action="store_true", help="Only populate ITAD historical prices"
+    )
@@
     args = parser.parse_args()
+
+    if args.refresh_current and args.itad_only:
+        parser.error("--refresh-current cannot be combined with --itad-only")

Also applies to: 770-776, 808-820

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/populate_prices.py` around lines 734 - 749, After parsing CLI args
(the parser variable and resulting args), validate and reject incompatible flag
combinations up front: if args.steam_only and args.itad_only call
parser.error(...) (or sys.exit non‑zero) with a clear message; similarly if
args.itad_only and (args.refresh_current or args.force_refresh) call
parser.error(...) because refresh flags apply to Steam only. Also move or guard
the Steam "refresh" banner/logic so it only runs when the Steam path will
execute (e.g., check not args.itad_only before printing the refresh banner or
performing Steam refresh in the code paths that reference
args.refresh_current/args.force_refresh).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/populate_prices.py`:
- Around line 195-215: The current retry branch treats all 4xx responses like
transient errors; change the logic so only transient statuses are retried: keep
429 and server-side errors (HTTP 5xx, e.g., 500–599; optionally 408) in the
retry path, but treat permanent client errors (400–499 excluding 429) as
immediate failures. Update the condition around response.status_code in the
request loop (the block that references response.status_code, url, attempt,
max_retries and calls self.adaptive_rate_limit) to: if status is 429 or (500 <=
status < 600) [and/or status == 408 if desired] then perform backoff/retry,
otherwise log/return None immediately for 4xx client errors so bad keys or
malformed requests fail fast.
- Around line 65-107: The constructor uses max_concurrent unchecked which allows
0 or negative values to create an unusable asyncio.Semaphore; validate
max_concurrent in __init__ (e.g., raise ValueError if max_concurrent < 1) before
creating self.semaphore, referencing the __init__ method, self.semaphore and
max_concurrent; additionally validate the CLI input in main()/argparse (after
parse_args(), reference args.concurrent and the parser) and call
parser.error("--concurrent must be >= 1") (or equivalent) to fail fast with a
clear message.

---

Outside diff comments:
In `@scripts/populate_prices.py`:
- Around line 734-749: After parsing CLI args (the parser variable and resulting
args), validate and reject incompatible flag combinations up front: if
args.steam_only and args.itad_only call parser.error(...) (or sys.exit non‑zero)
with a clear message; similarly if args.itad_only and (args.refresh_current or
args.force_refresh) call parser.error(...) because refresh flags apply to Steam
only. Also move or guard the Steam "refresh" banner/logic so it only runs when
the Steam path will execute (e.g., check not args.itad_only before printing the
refresh banner or performing Steam refresh in the code paths that reference
args.refresh_current/args.force_refresh).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1fcca3e0-e54a-4f22-8baf-20b4511dd31e

📥 Commits

Reviewing files that changed from the base of the PR and between cf89e09 and fcddeca.

📒 Files selected for processing (2)
  • justfile
  • scripts/populate_prices.py

Comment thread scripts/populate_prices.py
Comment thread scripts/populate_prices.py Outdated
SuaveIV added 2 commits March 27, 2026 16:36
Add validation for max_concurrent parameter to ensure it's at least 1.
Refine retry logic to only retry on transient errors (HTTP 429, 5xx, 408) and fail fast on client errors (4xx excluding 429/408).
Add command-line argument validation to prevent invalid flag combinations and enforce constraints.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/populate_prices.py`:
- Around line 795-798: The check that rejects the combination args.itad_only &&
args.force_refresh should be removed so --force-refresh can be used with
--itad-only; update the CLI parsing logic to allow that combo and ensure
downstream code passes args.force_refresh into populate_itad_prices (referenced
function populate_itad_prices(..., force_refresh=args.force_refresh)) so ITAD
cache refresh is performed, and remove or adjust the parser.error call that
currently blocks this case (the conditional containing args.itad_only and
args.force_refresh).
- Around line 129-159: The adaptive counters (self.success_counts and
self.error_counts) are lifetime totals so transient spikes get diluted; after
you compute error_rate and adjust delays in the adjustment block inside the
function (where current_delays, max_delays, min_delays, last_adjustment and
error_rate are used), reset the per-api counters (e.g.,
self.success_counts[api_type] and self.error_counts[api_type]) to zero (or
implement a sliding-window buffer) immediately after applying an adjustment and
updating self.last_adjustment[api_type] so future adjustments are based on
recent activity.
- Around line 311-439: The fetch_itad_price_single function currently masks real
request/errors as "not_found"; change it to return an explicit error status when
ITAD requests fail: inside fetch_itad_price_single, after any
make_request_with_retry call that returns None treat that as a failure and
return (app_id, "error", {}, "itad_request_failed") instead of continuing
silently, and in the two except blocks that catch exceptions (the ones with
logger.debug "ITAD appid lookup failed..." and "ITAD name search failed...")
return (app_id, "error", {}, "itad_exception") immediately rather than
swallowing the exception; this uses the existing function name
fetch_itad_price_single and make_request_with_retry to locate the changes and
ensures the final return only covers true "not_found" cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f4e49c9-6e45-464d-ada4-1ec449133d4c

📥 Commits

Reviewing files that changed from the base of the PR and between fcddeca and 7a28895.

📒 Files selected for processing (1)
  • scripts/populate_prices.py

Comment thread scripts/populate_prices.py
Comment thread scripts/populate_prices.py
Comment thread scripts/populate_prices.py Outdated
Comment on lines +795 to +798
if args.itad_only and args.force_refresh:
parser.error(
"--force-refresh applies to Steam only, incompatible with --itad-only"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t reject --itad-only --force-refresh.

populate_itad_prices(..., force_refresh=args.force_refresh) already supports refreshing cached ITAD data, and the CLI help says --force-refresh applies to all price data. This guard blocks the only ITAD-only refresh path.

🛠️ Minimal fix
-    if args.itad_only and args.force_refresh:
-        parser.error(
-            "--force-refresh applies to Steam only, incompatible with --itad-only"
-        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if args.itad_only and args.force_refresh:
parser.error(
"--force-refresh applies to Steam only, incompatible with --itad-only"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/populate_prices.py` around lines 795 - 798, The check that rejects
the combination args.itad_only && args.force_refresh should be removed so
--force-refresh can be used with --itad-only; update the CLI parsing logic to
allow that combo and ensure downstream code passes args.force_refresh into
populate_itad_prices (referenced function populate_itad_prices(...,
force_refresh=args.force_refresh)) so ITAD cache refresh is performed, and
remove or adjust the parser.error call that currently blocks this case (the
conditional containing args.itad_only and args.force_refresh).

Reset success/error counters when adjusting API rate limits to ensure
accurate tracking. Add None checks for ITAD API responses to prevent
errors and return explicit error statuses when requests fail or
exceptions occur. Remove obsolete --force-refresh validation that
conflicted with --itad-only usage.
@SuaveIV SuaveIV merged commit ce99ca5 into main Mar 27, 2026
1 check passed
@SuaveIV SuaveIV deleted the fix/consolidate-price-scripts branch March 27, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant