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.
Here are the following things I would like to improve:
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.
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’stests.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 interactionsutils/cube/_driver.py: the main driver program that will gather, parse, and write all tehh datautils/exceptions.py: custom exceptions fileHere is a crudely drawn diagram of what, at a high level, is happening in this app.

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:
"""
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! 😄
Let’s get into the fun stuff - flags! I want to create three flags:
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.
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.
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.
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:
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.
from utils.flags import Flags
Let’s also write a function that determines if there are any flags in the args.
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:
self.options = []
Now the store function:
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:
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.
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:
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:
if cli.has_flags(args) is True:
cli.set_options(args)
Great! Okay, now we can move on to the 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.
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.
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.
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.
if '-r' in flags:
row_obj['desired_qty'] = self.calc_desired_qty(card["rarity"])
Finally, we’ll write the row.
writer.writerow(row_obj)
The final function will look like this:
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:
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:
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!")
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!