supercontest.dbsession.commits

The writes: each function stages one change in the caller’s session.

Every function takes the caller’s AsyncSession first, and flushes, never commits. The route, job or script that owns the unit of work commits it once, when every write in it has succeeded – so a failure part-way leaves the database as it was, never half-written: a user’s old picks deleted with the new ones not inserted, or a week’s card with some of its games missing (which the lines job would then treat as complete, forever). It also keeps a row lock taken inside the unit (lock_user()) until the unit ends.

A flush is enough for everything that follows inside the unit: the database assigns ids and runs its triggers at the flush, and later reads in the same transaction see what it wrote. Sessions do not expire on commit (supercontest.dbsession.engine), so an object keeps what it held – and where the database rewrote a row the ORM did not write, the function that caused it refreshes that row before returning (_reload()).

Module Attributes

EmailJob

The scheduled broadcasts, as EmailSend names them.

Functions

add_user_to_current_paid_league

Adds a user to the current season's paid league, once.

change_game_status

Changes the status of a game.

claim_email_send

Claims a broadcast for a week, unless something already has.

delete_rows

Wrapper around deleting rows.

insert_contestants

Writes the lines and initial opponent info to the database.

lock_user

Row-locks a user until the transaction ends.

release_connection

Ends a read-only transaction, handing its connection back to the pool.

set_superuser

Grants or revokes admin for a known user.

write_picks

Deletes, updates, and inserts pick objects to the database as necessary.

write_scores

Commits the scores to the database (insert and/or update).

Classes

ScoresWrite

What write_scores() did.

supercontest.dbsession.commits.EmailJob

The scheduled broadcasts, as EmailSend names them.

alias of Literal[‘picks_open’, ‘picks_closing’, ‘all_picks’]

class supercontest.dbsession.commits.ScoresWrite(changed: bool, finished: list[Game])

Bases: NamedTuple

What write_scores() did.

changed: bool

Whether anything was written, and so whether there is anything to commit.

finished: list[Game]

The games this write finished.

async supercontest.dbsession.commits.add_user_to_current_paid_league(session: AsyncSession, user: User) → bool

Adds a user to the current season’s paid league, once.

Idempotent, because its main caller is Stripe’s webhook, which redelivers an event it did not see answered – and a second payment session from the same user is possible too. A repeat is a no-op, not a duplicate row: the membership table’s primary key would refuse that with an IntegrityError, turning a redelivery into a 500 that Stripe keeps retrying for days.

The caller’s commit drops the request’s memoized answers (supercontest.dbsession.memo), so from then on the new member is seen as paid, by the rest of the request and by every request after it.

Parameters:
  • session – The session to write through.

  • user – The user to add.

Returns:

True if they were added; False if they were already a member.

Raises:

ValueError – Outside a season, when there is no current paid league.

async supercontest.dbsession.commits.change_game_status(session: AsyncSession, game_id: int, status_name: str) → None

Changes the status of a game.

Parameters:
  • session – The session to write through.

  • game_id – The ID of the matchup that you’re changing the status for.

  • status_name – The name of the status to change to (via FK to statuses table).

async supercontest.dbsession.commits.claim_email_send(session: AsyncSession, job: Literal['picks_open', 'picks_closing', 'all_picks'], season: int, week: int, recipients: int) → bool

Claims a broadcast for a week, unless something already has.

One INSERT ... ON CONFLICT DO NOTHING on the table’s primary key, so two attempts at once cannot both win. The caller commits the claim before it sends, so a retry arriving mid-send already sees it.

Parameters:
  • session – The session to write through.

  • job – Which broadcast.

  • season – Season year.

  • week – Week number.

  • recipients – How many addresses it is going to.

Returns:

True if this call claimed it; False if it was claimed before.

async supercontest.dbsession.commits.delete_rows(session: AsyncSession, rows: Sequence[Base]) → None

Wrapper around deleting rows. You may delete one or multiple.

Parameters:
  • session – The session.

  • rows – The rows to delete.

async supercontest.dbsession.commits.insert_contestants(session: AsyncSession, week_id: int, lines: list[Line]) → None

Writes the lines and initial opponent info to the database. All cleaning is done before this.

Parameters:
  • session – The session to write through.

  • week_id – The ID of the week to write lines for.

  • lines – The week’s lines, from the card or entered by hand.

async supercontest.dbsession.commits.lock_user(session: AsyncSession, user_id: int) → None

Row-locks a user until the transaction ends.

Serializes one user’s pick writes. The pick rules read the user’s current picks and then write, so two submissions at once – two tabs, a retried request – would each see the other’s picks missing and could together exceed the weekly limit. With the lock, the second waits for the first to commit and then reads its picks.

Parameters:
  • session – The session whose transaction holds the lock.

  • user_id – The user whose writes to serialize.

async supercontest.dbsession.commits.release_connection(session: AsyncSession) → None

Ends a read-only transaction, handing its connection back to the pool.

For a caller about to wait on something outside the database – Westgate, ESPN, Stripe, SES. A session holds its connection from its first query until its transaction ends, idle in transaction the whole wait, and a worker’s pool is three connections: one slow fetch and one broadcast would pin most of it. The next query opens a fresh transaction; loaded objects stay usable, because sessions do not expire them on commit.

Only between reads. With a change staged it refuses, rather than commit a unit of work in the middle.

Parameters:

session – The session.

Raises:

RuntimeError – If the session holds unflushed changes.

async supercontest.dbsession.commits.set_superuser(session: AsyncSession, user_email: str, is_superuser: bool = True) → None

Grants or revokes admin for a known user.

Replaces adding a user to the Admin role, which is the is_superuser column now.

Parameters:
  • session – The session to write through.

  • user_email – The user to change.

  • is_superuser – Whether they should be an admin.

async supercontest.dbsession.commits.write_picks(session: AsyncSession, old_pick_objs_update: list[Pick], new_pick_contestant_objs: list[Contestant], user_id: int) → None

Deletes, updates, and inserts pick objects to the database as necessary.

The caller already checked which existing picks to keep. So we have a remainder to update.

  • If there’s the same number of existing picks and new picks, it will update them.

  • If there are more existing picks than new picks, it will update for the new and delete the remaining rows from existing.

  • If there are more new picks than existing picks, it will update the existing and then insert the remainder for the new.

All validity of the picks must be analyzed prior to this function.

Parameters:
  • session – The session.

  • old_pick_objs_update – Existing pick objects to update.

  • new_pick_contestant_objs – Contestant objects for the new picks.

  • user_id – The ID of the user submitting these picks.

async supercontest.dbsession.commits.write_scores(session: AsyncSession, games: list[Game], scores: list[ScoresFetch]) → ScoresWrite

Commits the scores to the database (insert and/or update).

Games and scores must be in the same corresponding order.

Parameters:
Returns:

Whether it wrote anything, and which games it finished.

Raises:

ValueError – If one of home/visitor doesn’t match favorite/underdog.