Let’s Update My Magic The Gathering Cube Generation Program, Part One

Sun Nov 02 2025

What We’re Working On

Today we’re going to be updating an existing program I wrote earlier this year that I call mtg_cube_csv_gen. The idea was borne out of my own desire to make a Cube using cards from the Magic the Gathering Onslaught block.

If you don’t play Magic, I am sure I already lost you. I would recommend playing the game! It’s awesome! This specific format is a little too “in the weeds” for a new player, but you may still get something out of this blog regardless! If you are familiar with Magic but not with Cube Draft, I would recommend checking out the Cube Draft page on mtg.wiki and then try it out. The format rules. The gist of the program is it uses an api called Scryfall to generate a customized csv that I can then use to upload to cubecobra which is a site centered around this format.

mtg_cube_csv_gen is a command line tool, and it works by simply running the program and passing what is known as a “set code” to generate the csv of cards. The csv includes properties one may find important, like the card’s name, collector number, the card colors, the rarity, the desired quantity based on the rarity, whether one owns the card, and the quantity owned.

The program itself is written in python and uses the requests library, along with the native csv module.

What We Want To Improve

Here are the following things I would like to improve:

  • Default card quantity to one
  • Allow duplicate exports of the same sets (with different file names)
  • Write some more tests
  • Add exception handling for the new features

The Program Now

Looking back on the code I wrote seven months ago, I am honestly pretty happy with myself and how well documented I made everything. I think this will make updating the code easy enough, though some things will undoubtedly have to be changed around.

The Structure

Here is the structure in v0.1.0:

.
├── app.py
├── out
├── README.md
├── requirements.txt
├── tests.py
└── utils
    ├── __init__.py
    ├── cli_util.py
    ├── cube_driver.py
    └── exceptions.py
  • app.py: The intro point of our program
  • /out: the output folder for the csv’s
  • tests.py: our test suite
  • /utils: all our utility programs/modules out of which our main program will be build
  • /utils/__init__.py: the module declaration for utils
  • /utils/cli/_util.py: a utility to handle command line interactions
  • utils/cube/_driver.py: the main driver program that will gather, parse, and write all tehh data
  • utils/exceptions.py: custom exceptions file

Here is a crudely drawn diagram of what, at a high level, is happening in this app.

Improvements

Default Card Quantity

Alright, this is easy enough. The card count is calculated by the function calc_desired_qty() in /utils/cube_driver.py. It is used when writing the csv on in the write_csv() function, attached below:

cube_driver.py
       """
       builds and outputs a .csv file
       """
       with open(f'./out/{self.set_code.lower()}_cards.csv', 'w', newline='') as csvfile:
           fieldnames = [
               'card_name',
               'collector_num',
               'colors', 'rarity',
               'desired_qty',
               'owned',
               'qty_owned']
           writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

           writer.writeheader()
           for card in self.cards:
               writer.writerow({
                   'card_name': card["card_name"],
                   'collector_num': card["collector_num"],
                   'colors' : card["colors"],
                   'rarity': card["rarity"],
                   'desired_qty' : self.calc_desired_qty(card["rarity"]),
                   'owned': 'N', #defaults to no
                   'qty_owned': 0,
               })

In the desired quantity field, we’re going to simply change the line desired_quantity: self.calc_desired_qty(card[“rarity”)] to: 'desired_qty' : 1,

Easy enough! Let’s make sure it actually works.

In the root folder of the project I’ll run on the CLI:

source venv/bin/activate
python3 app.py ons

Let’s check the output:

Great! It works! 😄

Feature Flags

Let’s get into the fun stuff - flags! I want to create three flags:

  1. One that uses the rarity calculation that i already wrote (4 copies at common, 3 copies at uncommon and one at rare and one at mythic.
  2. One that allows a custom distribution across the board. Ie, two copies of every card.
  3. Exclude specific collector numbers from the sets

Currently, however, the program only allows a certain number of arguments, so before we write the flags we’re going to have to allow an arbitrary number of flags, but we’re also going to want to handle any bad arguments that are passed.

Scaffolding

After some research and thought about how I want this to work, I decided to create a module that will specifically deal with feature flags. In the /utils folder I created a new file called flags.py.

In the __init__() function, we’ll add an array to define the flags that we can easily update if/when we need to.

flags.py
class Flags:
    def __init__(self)-> None:
        self.flags = [
            '-r', # Rarity <no args>
            '-cq', # Custom Quantity <single arg>
            '-e', # excluding <arbitrary num of arguments>
        ]

Cool, before we move on lets just write a quick validator function along with a getter function for the array.

flags.py
   def flag_is_valid(self, flag:str)-> bool:
       if flag.lower() in self.flags:
           return True
       return False

Nothing too crazy there, just a comparison inside a for loop. Just a note: I am using the lower() method in this because I want users to be able to not worry about capitalization when using the flags.

Here’s the getter:

flags.py
    def get_flags(self)->list:
        return self.flags

Okay, so we’re going to want to modify the CLIUtil to have an expected number of args that will be determined by the flag class. To start, let import the flags module In cli_util.py.

cli_util.py
from utils.flags import Flags

Let’s also write a function that determines if there are any flags in the args.

cli_util.py
   def has_flags(self, args:list)->bool:
       flag_list = self.f.get_flags()
       for arg in args:
           if arg in flag_list:
               return True
       return False

We’re also going to want to store what flags have been passed. So, we’re going to add the following line to the __init__()function:

cli_util.py
self.options = []

Now the store function:

cli_util.py
   def set_options(self, options:list)-> None:
       for option in options:
           self.options.append(option)

We’re going to refine this a bit later. We’re also going to want to get the options, so let’s add a getter:

cli_util.py
   def get_options(self)->list:
       return self.options

Lastly (for now) I am going to change the validate_num_arts() function so that it returns true so long as there are two or more arguments passed to it.

cli_util.py
def validate_num_args(self, args: str)->bool:
       return len(args) >= 2

Now, let’s move briefly to the app.py file and change around some high level stuff that’s going on.

Let’s get rid of the condition on line 16 that throws an exception if there are more than two arguments.

That leaves us with this:

app,py
import sys
from utils import cube_driver, cli_util
from utils.exceptions import TooFewArgumentsError, TooManyArgumentsError

cli = cli_util.CLIUtil()

try:
   args = sys.argv
   SET_CODE = ''
  
   if cli.validate_num_args(args) is True:
       SET_CODE = args[1]
   else:
       if len(args) < 2:
           raise TooFewArgumentsError
except TooFewArgumentsError as e:
   print(e)
except TooManyArgumentsError as e:
   print(e)

if cli.validate_set_code(SET_CODE) is True:
   driver = cube_driver.CubeDriver(SET_CODE)
   print(f'gathering cards in set: {driver.set_code}')
   driver.add_all_cards()
   print(f'writing to ./out/{driver.set_code}_cards.csv')
   driver.write_csv()
   print("complete!")

Now let’s start checking for flags. Underneath SET_CODE = args[1] we’ll add the following:

app.py
if cli.has_flags(args) is True:
  cli.set_options(args)

Great! Okay, now we can move on to the rarity flag.

Rarity Flag

This is going to be the simplest one to do I think. Let’s head back over to cli_util.py and edit the set_options() function to be more precise. Since we’re focusing on the rarity flag right now, let’s just worry about that.

We’ll check if -r is in the options array that we pass to the function, and if it is, we’ll add it to the options array of the CliUtil class.

cli_util.py
   def set_options(self, options:list)-> None:
       for option in options:
           if option == '-r':
               self.options.append('-r')
           if option == '-cq':
               ## validate flag parameter
               pass
           if option == '-e':
               ## validate flag parameter
               pass

I added placeholders for the two other arguments, but we’ll get to those later. Okay, so now if -r is passed to the program, it will be added to the helper’s “memory”.

Now, I think we can move to cube_driver.py and put it together.

In the cube_driver.py we’re going to change the write_csv() function so that it accepts an optional argument called *flags. The asterisk denotes that it is an optional argument.

cube_driver.py
def write_csv(self, *flags)-> None:

We’re also going to change around the structure of this. Inside the card loop we’re going to break out the object that is in the writer.write_row() function and make it its own object.

cube_driver.py
  for card in self.cards:
      row_obj ={
          'card_name': card["card_name"],
          'collector_num': card["collector_num"],
          'colors' : card["colors"],
          'rarity': card["rarity"],
          'desired_qty' : 1,
          'owned': 'N', #defaults to no
          'qty_owned': 0,
      }

Then we’ll add the control conditions.

cube_driver.py
if '-r' in flags:
        row_obj['desired_qty'] = self.calc_desired_qty(card["rarity"])

Finally, we’ll write the row.

cube_driver.py
writer.writerow(row_obj)

The final function will look like this:

cube_driver.py
def write_csv(self, *flags)-> None:
       """
       builds and outputs a .csv file
       """
       with open(f'./out/{self.set_code.lower()}_cards.csv', 'w', newline='') as csvfile:
           fieldnames = [
               'card_name',
               'collector_num',
               'colors', 'rarity',
               'desired_qty',
               'owned',
               'qty_owned']
           writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

           writer.writeheader()
           for card in self.cards:
               row_obj ={
                   'card_name': card["card_name"],
                   'collector_num': card["collector_num"],
                   'colors' : card["colors"],
                   'rarity': card["rarity"],
                   'desired_qty' : 1,
                   'owned': 'N', #defaults to no
                   'qty_owned': 0,
               }
               if '-r' in flags:
                       row_obj['desired_qty'] = self.calc_desired_qty(card["rarity"])
                      
               writer.writerow(row_obj)

Let’s plug it in to the app. We’ll move over to app.py and change line that says driver.write_csv() to this:

app.py
   if len(cli.get_options())==0:
       driver.write_csv()
   if len(cli.get_options())>=1:
       flags=cli.get_options()
       driver.write_csv(*flags)

app.py as a whole looks like:

app.py
import sys
from utils import cube_driver, cli_util
from utils.exceptions import TooFewArgumentsError, TooManyArgumentsError

cli = cli_util.CLIUtil()

try:
   args = sys.argv
   SET_CODE = ''
  
   if cli.validate_num_args(args) is True:
       SET_CODE = args[1]
       if cli.has_flags(args) is True:
           cli.set_options(args)
   else:
       if len(args) < 2:
           raise TooFewArgumentsError
except TooFewArgumentsError as e:
   print(e)
except TooManyArgumentsError as e:
   print(e)

if cli.validate_set_code(SET_CODE) is True:
   driver = cube_driver.CubeDriver(SET_CODE)
   print(f'gathering cards in set: {driver.set_code}')
   driver.add_all_cards()
   print(f'writing to ./out/{driver.set_code}_cards.csv')
   if len(cli.get_options())==0:
       driver.write_csv()
   if len(cli.get_options())>=1:
       flags=cli.get_options()
       driver.write_csv(*flags)
   print("complete!")

Making Sure It Works

Let’s make sure it’s all good. We’ll run the following command:

 python3 app.py ons -r
>> gathering cards in set: ons
>> writing to ./out/ons_cards.csv
>> complete!

Great! Let’s just check the file:

Yahoo! 🎉 Now let’s run it without the flag to make sure we’re good.

python3 app.py ons

It works! The rarity flag is complete!

We still have to do the remaining flags, but I am going to do that in part 2, as this turned out to be a bit more of a read than I initially anticipated. I’m not going to to merge it into my main branch yet either, but you can find the working branch here.

See ya in the next one!

All Rights Reserved © 2025