Source code for humble_steam_key_redeemer.core.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.
"""
from __future__ import annotations
from datetime import UTC, datetime
from enum import StrEnum
from sqlmodel import Field, SQLModel
def _utcnow() -> datetime:
"""Return the current UTC time."""
return datetime.now(UTC)
[docs]
class KeyState(StrEnum):
"""Lifecycle state of a Humble key."""
UNREVEALED = "unrevealed"
REVEALED = "revealed"
ATTEMPTED = "attempted"
REDEEMED = "redeemed"
ALREADY_OWNED = "already_owned"
FAILED = "failed"
SKIPPED = "skipped"
[docs]
class KeyRecord(SQLModel, table=True):
"""A key entry from a Humble order.
Attributes:
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.
"""
__tablename__ = "keys"
id: int | None = Field(default=None, primary_key=True)
gamekey: str = Field(index=True)
machine_name: str = Field(index=True)
human_name: str
key_type: str | None = Field(default=None, index=True)
steam_app_id: int | None = Field(default=None, index=True)
redeemed_key_val: str | None = Field(default=None)
# Position of the entry within its Humble order; required when asking
# Humble to reveal a key from a multi-key bundle entry.
key_index: int | None = Field(default=None)
is_gift: bool = False
is_expired: bool = False
state: KeyState = Field(default=KeyState.UNREVEALED, index=True)
matched_app_id: int | None = None
matched_app_name: str | None = None
match_score: int | None = None
created_at: datetime = Field(default_factory=_utcnow)
updated_at: datetime = Field(default_factory=_utcnow)
@property
def is_steam(self) -> bool:
"""Whether this entry is a Steam key."""
return (self.key_type or "").lower() == "steam" or self.steam_app_id is not None
@property
def is_revealed(self) -> bool:
"""Whether Humble has released the key value."""
return bool(self.redeemed_key_val)
[docs]
class RedemptionAttempt(SQLModel, table=True):
"""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.
"""
__tablename__ = "redemption_attempts"
id: int | None = Field(default=None, primary_key=True)
key_id: int = Field(foreign_key="keys.id", index=True)
result_code: int
result_name: str
detail: str
succeeded: bool = Field(index=True)
granted_items: str | None = None
attempted_at: datetime = Field(default_factory=_utcnow)
__all__ = ["KeyRecord", "KeyState", "RedemptionAttempt"]