Creating A League Manager With Python
It Sure Beats A To-Do App
For well over a decade, I had been participating in a small recreational soccer league, up until I tore my ACL. I had previously volunteered to help build a website for the league, as it is severely lacking in that department. Well, I'm still working on it...
Background
This is the part where I tell you about how I started thinking I was going to build a simple static site that allowed the league organizers to use some spreadsheets to import schedules, results, and standings which they track by hand.
It should have been pretty straight forward.
But then I thought... how cool would it be if the organizers could just enter stuff directly on a website after the matches are done, and have the standings updated real time. Or what if I could make it so that they could generate random schedules at the start of each season?
That would be pretty simple, right?
Wrong
And to prove that I'm a glutton for punishment... this project sounds perfect for a Django application. Having an admin section built in that I could use, or teach the organizers to use, what's there to lose?
Well, how about just building everything from scratch, because... huh?
Nevermind that.
I figured the actual hardest part of all this was actually going to be in the database modeling. Understanding the relationship between seasons, leagues, matches, results, standings, schedules, teams, players, etc...
I started thinking about this over 6 months ago, but most of the time I was just working things out in my head.
This is where not choosing Django might have been a good idea. My impulse when I sit down to code is to ... start coding ... before having everything worked out. And Django makes that very easy.
Modeling
Instead, I built some attrs models to represent what I wanted in Python only. This made it easier to think about the data I was going to be using internally.
I then slowly introduced SQLAlchemy to the mix. However, with SQLAlchemy 2.0, you're generally encouraged to map your Python models directly to their corresponding database fields. For reasons I won't get into here, I decided to use "imperative mapping" instead.
This seems to create a bit more code in the long run, but helps me keep the Python models separate from the database table metadata.
Alchemy
I quickly settled on using Advanced Alchemyas an ORM helper. The library is a companion to SQLAlchemy and Alembic, which I wrote about previously.
The short of it is that it dramatically reduces the amount of boilerplate you would need to write for a CRUD (create, read, update, delete) application. Translation: I didn't have to write any logic for basic CRUD operations at all.
This allowed me to focus more on the actual app logic that concerns running a league or leagues.
Modular
Another reason I didn't mention as to why I didn't go with Django this time is because I wanted this tool to be agnostic of web frameworks. In fact, I wanted it to work within a very straightforward CLI application.
As such, I built a Typer interface (and borrowed the Alembic commands from Advanced Alchemy) and included a way to populate some example synthetic data straight from the command line.
Services
Since most of the time, you are operating on specific database tables, the interface to these operators is held within "repositories" and "services" subclassed from the Advanced Alchemy classes.
However, I wanted to create as little friction as possible for interacting with databases.
One of the ways was by streamlining the initial migration workflow.
The second was by stashing away all these services into an svcs registry. In case you haven't tried it, svcs
is a service locator where you can register factories/values for your interfaces (such as these "repositories" and "services"), and allows you to retrieve them at will, with automatic cleanup done for you.
(Don't worry, I don't know what half of that means either.)
In the end, you're able to refer to the service by it's type (i.e., "SeasonSyncSevice"), and all the database operations for that table are provided to you at any point in the app lifecycle!
Codeberg
I decided to host my repository on Codeberg.
That's it. That's the section.
Oh, wait... here's the League Manager repository.
What Does It Look Like?
Well, this is the pitch from the README:
League Manager allows you to create and manage seasons, leagues, teams, players, and more.
First, define a season that establish when the competition will begin. Next, create one or more leagues (and corresponding teams) to attach to that season.
Then you will be able to auto-generate a weekly schedule based on the number of fixtures/matches within that season.
You can also create one-off fixtures/matches on the schedule. Track results and auto-update a standings table.
You can either clone the project and use it with the CLI, or you can pip install it (currently only from the repository), and use it with your own project.
If you do the latter, this is how it would work.
Create a new project:
# Create a new project and virtual environment
mkdir my-project
cd my-project
python -m venv .venv
# On Linux/MacOS
source .venv/bin/activate
# Or Windows
.venv/Scripts/activate
Install from the repo:
python -m pip install --pre https://codeberg.org/pythonbynight/league-manager.git
Or if you use uv
:
uv add league-manager @ https://codeberg.org/pythonbynight/league-manager.git
Then, run the following database operations:
# Creates a "migrations" directory and alembic files
mgr db init migrations
# Creates the actual migrations
mgr db make-migrations
# Actually builds the tables into the database
mgr db upgrade
Then, to populate the fake data into your database table (sqlite
by default, by the way):
# Populate the database with synthetic data
mgr populate
And, if you want to create a web app, or something or another, you can import leaguemanager
and take advantages of the services.
For example, here's how to do it with Litestar:
from leaguemanager.dependency import LeagueManagerRegistry
from leaguemanager.services import SeasonSyncService, TeamSyncService
from litestar import Litestar, get
registry = LeagueManagerRegistry()
@get("/")
def hello_world() -> dict:
season_service = registry.provide_db_service(SeasonSyncService)
team_service = registry.provide_db_service(TeamSyncService)
number_of_seasons = season_service.count()
number_of_teams = team_service.count()
seasons = season_service.list()
return {
"seasons": number_of_seasons,
"first_season_name": seasons[0].name,
"teams": number_of_teams
}
But you can insert your web framework of choice just the same (in theory, I haven't tested for it).
If you boot up that app, you'll get a result that looks like:
{"seasons":2,"first_season_name":"Spring Season 2025 - Sundays","teams":8}
What Next?
Well, there's still lots to do, really. First off, this is currently primarily geared toward a soccer league, but obviously, you could make it fit your need as well.
I also will need a lot more documentation in terms of all the other models and services that are included with it, and what they can do.
It already supports schedule generation, minor stat tracking (wins/losses/draws etc...), and auto update for standings.
But links still need to be applied for players, teams, managers, etc...
So keep an eye out on the repo if you're interested.