core

Domain logic with no browser or network dependencies.

Engine

The redemption engine.

Decides which keys are worth an activation attempt and carries those attempts out. The ordering is deliberate and is the whole point of the tool:

  1. Skip anything already owned on Steam. Steam counts a duplicate as a failure, and failures are limited to roughly ten per hour against about fifty successes, so an unchecked run exhausts its budget on games the account already has.

  2. Reveal on Humble only what is about to be redeemed, because revealing forfeits the ability to make a gift link and cannot be undone.

  3. Stop at the first rate limit rather than retrying into a deeper cooldown.

class humble_steam_key_redeemer.core.engine.PlanEntry(record, decision, skip_reason=None)[source]

Bases: object

One key’s disposition ahead of redemption.

Variables:
  • record – The key under consideration.

  • decision – Ownership match against the Steam library.

  • skip_reason – Why the key will not be attempted, when applicable.

Parameters:
record: KeyRecord
decision: MatchDecision
skip_reason: str | None
property will_attempt: bool

Whether this key will be sent to Steam.

class humble_steam_key_redeemer.core.engine.RedemptionEngine(store, steam)[source]

Bases: object

Plans and performs Steam redemptions for Humble keys.

Parameters:
  • store (RedeemerStore) – Local store of keys and attempts.

  • steam (SteamGateway) – Steam gateway used for ownership and redemption.

plan(records, *, match_threshold=70, confirm_threshold=95)[source]

Decide which keys are worth attempting.

Parameters:
  • records (Sequence[KeyRecord]) – Candidate keys.

  • match_threshold (int) – Score above which a title counts as owned.

  • confirm_threshold (int) – Score at or above which no review is needed.

Return type:

RedemptionPlan

Returns:

The resulting RedemptionPlan.

redeem(plan, *, reveal=None, on_result=None, limit=0)[source]

Attempt every key the plan selected.

Parameters:
Return type:

RunSummary

Returns:

A RunSummary of the run.

class humble_steam_key_redeemer.core.engine.RedemptionPlan(entries=<factory>)[source]

Bases: object

The set of decisions for a run.

Variables:

entries – Every considered key, in input order.

Parameters:

entries (list[PlanEntry])

entries: list[PlanEntry]
property to_attempt: list[PlanEntry]

Entries that will be attempted.

property skipped: list[PlanEntry]

Entries that will be skipped.

property uncertain: list[PlanEntry]

Entries that matched an owned game without clearing confirmation.

These are attempted rather than skipped, because a wrong skip wastes a redeemable key outright. They are surfaced so the reason for an “already owned” result is visible rather than surprising.

class humble_steam_key_redeemer.core.engine.RunSummary(redeemed=0, already_owned=0, failed=0, skipped=0, rate_limited=False)[source]

Bases: object

Outcome counts for a completed run.

Parameters:
  • redeemed (int)

  • already_owned (int)

  • failed (int)

  • skipped (int)

  • rate_limited (bool)

redeemed: int
already_owned: int
failed: int
skipped: int
rate_limited: bool
class humble_steam_key_redeemer.core.engine.SteamGateway(*args, **kwargs)[source]

Bases: Protocol

The Steam operations the engine depends on.

Defined structurally so the engine can be exercised without a Steam account, and so the concrete connector stays swappable.

list_owned_apps()[source]

Return owned Steam applications by id.

Return type:

dict[int, str]

redeem_key(key)[source]

Redeem one product key.

Return type:

dict[str, object]

Parameters:

key (str)

Matching

Match Humble titles against an owned Steam library.

Humble and Steam rarely spell a title identically — “Game GOTY” against “Game: Game of the Year Edition”, trademark symbols, differing punctuation — so ownership is decided by fuzzy comparison rather than equality.

Getting this wrong is expensive in both directions. A missed match spends one of Steam’s ten hourly failed activations on a game the account already owns; a false match silently skips a key that could have been redeemed.

class humble_steam_key_redeemer.core.matching.MatchDecision(app_id, app_name, score, confident)[source]

Bases: object

The outcome of comparing one title against the owned library.

Variables:
  • app_id – Steam application id of the best candidate, if any.

  • app_name – Name of that application.

  • score – Similarity score from 0 to 100.

  • confident – Whether the score clears the confirmation threshold.

Parameters:
  • app_id (int | None)

  • app_name (str | None)

  • score (int)

  • confident (bool)

app_id: int | None
app_name: str | None
score: int
confident: bool
property matched: bool

Whether any candidate cleared the match threshold.

class humble_steam_key_redeemer.core.matching.OwnershipMatcher(owned, *, threshold=70, confirm_threshold=95)[source]

Bases: object

Decides whether a Humble title is already owned on Steam.

Parameters:
  • owned (dict[int, str]) – Mapping of Steam app id to application name.

  • threshold (int) – Score above which a title counts as a candidate match.

  • confirm_threshold (int) – Score at or above which a match needs no review.

match(title, *, steam_app_id=None)[source]

Find the best owned application matching title.

A Steam app id reported by Humble is authoritative and short-circuits the fuzzy comparison entirely.

Parameters:
  • title (str) – Humble display title.

  • steam_app_id (int | None) – Steam app id from Humble, when present.

Return type:

MatchDecision

Returns:

The best MatchDecision; unmatched when nothing clears the threshold.

candidates(title, *, limit=5)[source]

Return the closest owned applications for manual review.

Parameters:
  • title (str) – Humble display title.

  • limit (int) – Maximum number of candidates.

Return type:

list[MatchDecision]

Returns:

Candidate matches, best first.

Models

Database models for keys and redemption attempts.

Replaces the append-only CSV files the tool previously used. CSV was doing double duty as both report and state, which made it the source of two problems: fields containing commas, quotes, or newlines corrupted the file, and values beginning with =, +, - or @ were interpreted as formulas when the file was opened in a spreadsheet.

A database keeps state; CSV export remains available as a report, where values are escaped on the way out.

class humble_steam_key_redeemer.core.models.KeyRecord(*, id=None, gamekey, machine_name, human_name, key_type=None, steam_app_id=None, redeemed_key_val=None, key_index=None, is_gift=False, is_expired=False, state=KeyState.UNREVEALED, matched_app_id=None, matched_app_name=None, match_score=None, created_at=<factory>, updated_at=<factory>)[source]

Bases: SQLModel

A key entry from a Humble order.

Variables:
  • gamekey – Humble order identifier.

  • machine_name – Humble’s stable internal name for the entry.

  • human_name – Display title as shown by Humble.

  • key_type – Platform the key targets (steam, gog, …).

  • steam_app_id – Steam application id, when Humble reports one.

  • redeemed_key_val – The revealed key, once Humble has released it.

  • state – Current lifecycle state.

Parameters:
  • id (int | None)

  • gamekey (str)

  • machine_name (str)

  • human_name (str)

  • key_type (str | None)

  • steam_app_id (int | None)

  • redeemed_key_val (str | None)

  • key_index (int | None)

  • is_gift (bool)

  • is_expired (bool)

  • state (KeyState)

  • matched_app_id (int | None)

  • matched_app_name (str | None)

  • match_score (int | None)

  • created_at (datetime)

  • updated_at (datetime)

id: int | None
gamekey: str
machine_name: str
human_name: str
key_type: str | None
steam_app_id: int | None
redeemed_key_val: str | None
key_index: int | None
is_gift: bool
is_expired: bool
state: KeyState
matched_app_id: int | None
matched_app_name: str | None
match_score: int | None
created_at: datetime
updated_at: datetime
property is_steam: bool

Whether this entry is a Steam key.

property is_revealed: bool

Whether Humble has released the key value.

model_config = {'from_attributes': True, 'read_from_attributes': True, 'read_with_orm_mode': True, 'registry': PydanticUndefined, 'table': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class humble_steam_key_redeemer.core.models.KeyState(*values)[source]

Bases: StrEnum

Lifecycle state of a Humble key.

UNREVEALED = 'unrevealed'
REVEALED = 'revealed'
ATTEMPTED = 'attempted'
REDEEMED = 'redeemed'
ALREADY_OWNED = 'already_owned'
FAILED = 'failed'
SKIPPED = 'skipped'
class humble_steam_key_redeemer.core.models.RedemptionAttempt(*, id=None, key_id, result_code, result_name, detail, succeeded, granted_items=None, attempted_at=<factory>)[source]

Bases: SQLModel

One attempt to redeem a key on Steam.

Recording every attempt, rather than only the final state, means a rerun can tell “not tried yet” apart from “tried and rejected”, which is what keeps the tool from spending its scarce failure budget twice on the same dead key.

Parameters:
id: int | None
key_id: int
result_code: int
result_name: str
detail: str
succeeded: bool
granted_items: str | None
attempted_at: datetime
model_config = {'from_attributes': True, 'read_from_attributes': True, 'read_with_orm_mode': True, 'registry': PydanticUndefined, 'table': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Store

Persistent store for keys and redemption attempts.

The database is the tool’s memory across runs. It answers the question that decides whether a key is worth an activation attempt: has this key already been tried, and what did Steam say?

class humble_steam_key_redeemer.core.store.RedeemerStore(path)[source]

Bases: object

SQLite-backed store of keys and redemption attempts.

Parameters:

path (Path) – Database file path. Parent directories are created.

property path: Path

Path to the database file.

session()[source]

Open a new database session.

expire_on_commit is disabled so records stay readable after the session closes. Otherwise every attribute access on a returned record tries to reload from a session that no longer exists.

Return type:

Session

upsert_keys(records)[source]

Insert or update key records, keyed by Humble’s identifiers.

Humble’s (gamekey, machine_name) pair is stable across runs, so re-importing an order updates existing rows instead of duplicating them and losing their redemption history.

Parameters:

records (Iterable[KeyRecord]) – Key records to store.

Return type:

int

Returns:

The number of records written.

all_keys()[source]

Return every stored key.

Return type:

Sequence[KeyRecord]

steam_keys()[source]

Return stored keys that target Steam.

Return type:

list[KeyRecord]

pending_keys()[source]

Return Steam keys that are still worth attempting.

Excludes keys already redeemed, already owned, or previously attempted and rejected, so a rerun does not spend Steam’s failure budget on outcomes already known.

Return type:

list[KeyRecord]

update_key(record)[source]

Persist changes to a key record.

Parameters:

record (KeyRecord) – The record to save.

Return type:

None

record_attempt(attempt)[source]

Store one redemption attempt.

Parameters:

attempt (RedemptionAttempt) – The attempt to record.

Return type:

RedemptionAttempt

Returns:

The stored attempt, with its assigned id.

attempts_for(key_id)[source]

Return every recorded attempt for a key.

Parameters:

key_id (int) – The key’s database id.

Return type:

list[RedemptionAttempt]

export_csv(destination, *, steam_only=False)[source]

Write stored keys to a CSV report.

Uses csv for quoting so titles containing commas, quotes or newlines round-trip correctly, and neutralizes formula-leading values.

Parameters:
  • destination (Path) – Output file path.

  • steam_only (bool) – Restrict the report to Steam keys.

Return type:

Path

Returns:

The path written.

humble_steam_key_redeemer.core.store.csv_safe(value)[source]

Neutralize a value for CSV export.

Titles come from Humble and are not trusted input. A cell beginning with =, +, - or @ is executed as a formula by Excel, LibreOffice and Sheets, so such values are prefixed with an apostrophe to force text.

Parameters:

value (object) – Any cell value.

Return type:

str

Returns:

A string safe to place in a spreadsheet cell.