{ "info": { "author": "Esko-Kalervo Salaka", "author_email": "esko.salaka@gmail.com", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: Zope Public License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "# mtgtools\n\nmtgtools is a collection of tools for easy handling of **Magic: The Gathering** data on your computer. The card data\ncan be easily downloaded from **Scryfall** API or **magicthegathering.io** API and it is saved in a ZODB - database,\nwhich is a native object database for Python. Everything is simply in Python, so no knowledge of SQL or the likes is needed to work with the database.\n\n## Features\n\n- Easily download, update and save Magic: The Gathering card and set data from Scryfall and/or magicthegathering.io to\n a local ZODB database (native object database for Python). Updating the database from scratch usually takes about 2\n minutes on my computer.\n\n- Easily iterate, filter, sort, group and handle card lists, sets and decks. The usual searching methods on the whole\n card database of 40k cards take about 0.15s on my computer.\n\n- Save your own card lists and decks in a database in pure Python.\n\n- Read and write card lists or decks from files.\n\n- Generate random samples, random cards, booster packs etc. from any lists of cards.\n\n- Download card images of the type of your choice from Scryfall.\n\n- Create proxy image sheets from lists of cards using Scryfall.\n\n## Requirements\n\n- **Python 3.5** - mtgtools is tested on Python 3.5 but will probably also work on later versions\n\n- **ZODB** - Can be installed with `pip install zodb`. More info in http://www.zodb.org/en/latest/.\n\n- **requests** - Can be installed with `pip install requests`. More info in https://pypi.org/project/requests/.\n\n- **PIL** - Not necessary, but needed for creating proxy image sheets. Can be installed with `pip install pillow`\n\n## Scryfall vs magicthegathering.io\n\nAt the moment there exists two different kind of APIs for mtg data, Scryfall and magicthegathering.io. They are\nstructured in different ways and both have pros and cons. For example Scryfall cards contain attribute `card_faces`\nwhere as the faces in mtgio are separate cards.\n\nAt the moment Scryfall has a more extensive database with more useful data like prices and purchase uris and also hosts\ngood quality card images, so in my opinion it is more useful of the two.\n\n## Installing\n\nmtgtools can be simply installed with `pip install mtgtools`.\n\n## Usage guide\n\n### Persistent card, set and card list objects\n\nWorking with the database mostly revolves around working with the following persistent card and card list objects. Data\npersistence in this case basically means that ZODB will automatically detect when these objects are accessed and\nmodified and saves the according changes automatically when transactions have been committed.\n\nA good guide on ZODB can for example be found here: https://media.readthedocs.org/pdf/zodborg/latest/zodborg.pdf\n\n#### **PCard**\n\n`PCard` is a simple persistent dataclass representing Magic: the Gathering cards with their characteristic\nattributes. It is constructed simply with a json response dictionary from either magicthegathering.io or Scryfall\nAPI, so `PCard` has all the attributes matching the responses' keys and values.\n\nNote, that the attributes `power`, `toughness` and `loyalty` are saved as strings since they might contain characters\nlike '\\*' or 'X'. For convenience, the card objects will also contain numerical versions of these attributes:\n`power_num`, `toughness_num` and `loyalty_num`. This makes searching much easier in many cases. After stripping away\nthese non-digit characters, the remaining numbers will be in the numerical version of the attribute. If nothing is left\nafter stripping, the numerical version will be 0.\n\nAnother difference between Scryfall and mtgio is that in mtgio API attribute names are in camelCase style. For the\npurpose of consistency, the attributes in this software are transformed into snake_case which makes many of the\nattributes identical to the ones in Scryfall. For example, the attribute `manaCost` from mtgio has been changed to\n`mana_cost` which is the same as in Scryfall.\n\nFor more information on what attributes cards have, read\nhttps://scryfall.com/docs/api/cards for Scryfall card objects and\nhttps://docs.magicthegathering.io/#api_v1cards_list for magicthegathering.io card objects.\n\n#### **PCardList**\n\n`PCardList` is a persistent card list or deck object that mostly acts just like a normal Python list for `PCard`\nobjects. These lists can be saved in the database just like any other persistent objects, and a `PCardList` is used\nas a container for all the cards in the database.\n\n`PCardList` has many useful methods for querying, filtering, sorting and grouping it's contents and creating new card\nlists by combining other card lists in various ways. It also contains other handy methods like downloading the images\nof it's cards from Scryfall, creating proxy image sheets from it's cards, printing out it's contents in a readable\nway and creating deck-like strings or files of it's contents.\n\nExcept for the usual in-place list methods like `extend`, `append` and `remove` the `PCardList` is functional in\nstyle, meaning that calling any of the other filtering or querying methods return new `PCardList` objects leaving the\noriginal untouched.\n\n`PCardList` can also be used as a deck by adding cards to it's `sideboard`. Having cards in the sideboard\nchanges some functionalities of the methods like `deck_str` in which now also the sideboard cards are added. Images\nare downloaded and proxies created for both the cards and the sideboard. However, Having cards in the 'sideboard'\ndoes not change the behavior of the crucial internal methods like `len`, `getitem` or `setitem`,\nso basically the cards in the sideboard are a kind of an extra.\n\n#### **PSet**\n\n`Pset` is a simple Persistent dataclass representing Magic: The Gathering sets with their characteristic\nattributes. It is constructed simply with a json response dictionary from either magicthegathering.io or Scryfall\nAPI, so **PSet** has all the attributes matching the responses' keys and values.\n\nFor more information on what attributes sets have, read\nhttps://scryfall.com/docs/api/sets for Scryfall set objects and\nhttps://docs.magicthegathering.io/#api_v1sets_list for magicthegathering.io set objects.\n\nAdditionally, `PSet` inherits from `PCardList` and it also contains all the cards of the set. Working with `PSet`\nby for example querying it's cards returns new `PCardList` objects is safe for the set leaving it untouched.\n\n#### **PSetList**\n\n`PSetList` is a persistent set list object that mostly acts just like a normal Python list for `Pset` objects.\nThese lists can be saved in the database just like any other persistent objects. `PSetList` contains handy methods for\nquerying the sets it contains but in most cases it is only useful as a container database. It works very similarly\nto `PCardList` except that they hold sets rather than cards.\n\n### Working with the database\n\n#### Opening/creating databases\n\nAn existing database can be opened simply with\n\n```\n>>> from mtgtools.MtgDB import MtgDB\n>>> mtg_db = MtgDB('my_db.fs')\n```\n\nIf no storage in the given path is found, a new empty database is automatically created.\n\nNow that the connection to the database is open, the `mtg_db` will contain all the needed ZODB-related objects\n`storage`, `connection`, `database` and `root` (more about these in http://www.zodb.org/en/latest/reference/index.html).\nThe cards and sets can now be found in the `root` of the database with\n\n```\n>>> scryfall_cards = mtg_db.root.scryfall_cards\n>>> scryfall_sets = mtg_db.root.scryfall_sets\n```\n\nand\n\n```\n>>> mtgio_cards = mtg_db.root.mtgio_cards\n>>> mtgio_sets = mtg_db.root.mtgio_sets\n\n```\n\nAll the cards are saved as a `PCardList` and all the sets are saved as a `PSetList`. The root acts as a\nboot-strapping point and a top-level container for all the objects in the database.\n\n```\n>>> print(mtg_db.root)\n\n\n```\n\nThe above method for accessing the database objects is a convenience, and you can also access the root mapping with\n\n```\n>>> root_mapping = tool.connection.root()\n>>> print([key for key in root_mapping.keys()])\n\n['scryfall_sets', 'mtgio_cards', 'scryfall_cards', 'mtgio_sets']\n\n>>> print('scryfall_cards' in root_mapping)\n\nTrue\n```\n\n#### Updating\n\nBuilding the database from scratch from Scryfall and mtgio is simply done with\n\n```\nmy_db.scryfall_update()\nmy_db.mtgio_update()\n```\n\nThe update downloads and saves all new card and set data and also updates any changes to the existing data. This is\nalso useful when updating for example the price and legality attributes of the Scryfall cards which might often change.\n\nBuilding the database from scratch takes about few minutes to complete and it is mostly affected by the API request\nlimits which are 10 request per second for Scryfall and 5000 requests per hour for magicthegathering.io. About 10\nrequests per second are sent during updating which should comply with the Scryfall limits, and with magicthegathering.io\nyou have to make sure not to run the update too many times per hour.\n\n### Working with card lists\n\n#### Querying, filtering and sorting\n\n`PCardList` has two handy methods for \"querying\" its contents which return new `PCardList` objects:
\n\n`where(invert=False, search_all_faces=False, **kwargs)`\n\nand\n\n`where_exactly(invert=False, search_all_faces=False, **kwargs)`\n\n`where`, the looser method, returns a new `PCardList` for which _ANY of the given keyword arguments match 'loosely'_\nwith the attributes of the cards in this list. The arguments should be any card attribute names such as\n'_power_', '_toughness_' and '_name_'.\n\nString attributes are case insensitive and it is enough that the argument is a substring of the attribute.\n\nFor list attributes the _**order does not matter**_ and it is enough for _**one of the elements to match exactly**_.\n\nFor convenience, for numerical attributes it is enough that the argument is _**larger or equal**_ to the attribute.\n\n`where_exactly`, the stricter method, returns a new list of cards for which _**ALL the given keyword arguments match\ncompletely**_ with the attributes of the cards in this list.\n\nFor both of these methods, the results can be inverted to return all the cards NOT matching the arguments by setting\n`invert=True`.\n\nFor Scryfall cards, which sometimes have the `card_faces` attribute, normally only the first face of the card\n(the normal face you would play) is considered when matching arguments. By setting `search_all_faces=True` the arguments\ncan now also match with any possible faces of the cards.\n\nLet's start by getting all the Scryfall cards and sets of the database:\n\n```\n>>> from mtgtools.MtgDB import MtgDB\n\n>>> mtg_db = MtgDB('my_db.fs')\n>>> cards = mtg_db.root.scryfall_cards\n>>> sets = mtg_db.root.scryfall_sets\n```\n\nSome basic searching:\n\n```\n>>> werebears = cards.where_exactly(name='Werebear')\n>>> print(werebears)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody)]\n\n>>> print(len(werebears))\n\n4\n```\n\nTurns out that there are 4 different Werebear cards in 4 different sets. Lets get the single card from Odyssey:\n\n```\n>>> ody_werebear = cards.where_exactly(name='Werebear', set='ody')[0]\n>>> print(ody_werebear)\n\nWerebear (ody)\n\n>>> print(ody_werebear.name, ody_werebear.set, ody_werebear.set_name, ody_werebear.power, ody_werebear.toughness)\n\nWerebear ody Odyssey 1 1\n\n>>>print(ody_werebear.oracle_text)\n\n{T}: Add {G}.\nThreshold \u00e2\u20ac\u201d Werebear gets +3/+3 as long as seven or more cards are in your graveyard.\n```\n\nNote, that in this case using `cards.where_(name='Werebear', set='ody')` would not only return the Werebears but ALSO\nall the other cards from the set 'ody' since `where` returns the cards for which ANY of the given keyword arguments\nmatch partly or completely.\n\nAlso note, that for `where` it is enough for the arguments match only partly. For example with string arguments like\n`name`, `type_line`, and `oracle_text` it is enough for the argument to be a substring of the cards' attribute in\nquestion:\n\n```\n>>> werebears = cards.where(name='wereb')\n>>> print(werebears)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody)]\n\n>>> print(cards.where(oracle_text='12 damage'))\n\n[Everythingamajig (ust), Tower of Calamities (som)]\n```\n\nQuerying and other operations return new lists so we can also chain multiple queries together. One of he previous\nexamples with chaining:\n\n```\n>>> ody_werebear = cards.where_exactly(name='Werebear').where_exactly(set='ody')[0]\n>>> print(ody_werebear.uri)\n\nhttps://scryfall.com/card/ody/282?utm_source=api\n```\n\nIf the above methods are not enough to find what you need, then there is also the `filtered` - method which works\nquite like the usual `filter` - method for Python lists. `filtered` takes a function object and returns a new list\ncontaining all the cards of the list for which the given function returns True. Lambda functions are very convenient\nwith this method. For example, we can find the Odyssey _Werebear_ by filtering our cards in the following way:\n\n```\n>>> ody_werebear = cards.filtered(lambda card: card.name == 'Werebear' and card.set == 'ody')\n>>> print(ody_werebear)\n\n[Werebear (ody)]\n```\n\nThe card list can be sorted with the `sorted` - method, which works quite like the usual `sort` - method for Python\nlists. It takes a function object which should return some attributes of card objects by which this list is sorted.\nFor example sorting by set codes:\n\n```\n>>> werebears = cards.where_exactly(name='Werebear')\n>>> print(werebears)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody)]\n>>> sorted_werebears = werebears.sorted(lambda card: card.set)\n>>> print(sorted_werebears)\n\n[Werebear (ema), Werebear (ody), Werebear (td0), Werebear (wc02)]\n```\n\nFinally, the card objects in `PCardList` are stored in it's `cards` - attribute, in a `PersistentList` which can also\nbe accessed directly.\n\n#### Creating and combining lists\n\n`PCardList` acts like normal Python list so we can use normal indexing and slicing. You can also create empty lists:\n\n```\n>>> werebears = cards.where_exactly(name='Werebear')\n>>> print(werebears[1:3])\n\n[Werebear (td0), Werebear (wc02)]\n\n>>> print(len(werebears[1:3]))\n\n2\n\n>>> print(werebears[-1])\n\nWerebear (ody)\n\n>>> from mtgtools.PCardList import PCardList\n>>> new_empty_list = PCardList()\n>>> print(new_empty_list)\n\n[]\n\n>>> print(len(new_empty_list))\n\n0\n```\n\nCards can be easily combined with addition. Addition works with lists and single card objects:\n\n```\n>>> werebears = cards.where_exactly(name='Werebear')\n>>> wild_mongrels = cards.where_exactly(name='Wild Mongrel')\n>>> two_bears_One_mongrel = werebears + wild_mongrels[0]\n>>> print(two_bears_One_mongrel)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody), Wild Mongrel (gvl)]\n\nTwo_bears_two_mongrels = werebears[0:2] + wild_mongrels[0:2]\nprint(Two_bears_two_mongrels)\n\n[Werebear (ema), Werebear (td0), Wild Mongrel (gvl), Wild Mongrel (vma)]\n```\n\nAnother way of combining lists is to append cards to an existing list. Note, that this will actually change the list\ninstead of creating another one:\n\n```\n>>> werebears = cards.where_exactly(name='Werebear')\n>>> one_mongrel = cards.where_exactly(name='Wild Mongrel')[0]\n>>> werebears.append(one_mongrel)\n>>> print(werebears)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody), Wild Mongrel (gvl)]\n```\n\nCards can be removed from lists with subtraction or with the common list method `remove`. Subtraction works with lists\nand single card objects and it is basically the same as set subtraction:\n\n```\n>>> werebears = cards.where_exactly(name='Werebear')\n>>> wild_mongrels = cards.where_exactly(name='Wild Mongrel')\n>>> two_bears_One_mongrel = werebears + wild_mongrels[0]\n>>> print(two_bears_One_mongrel)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody), Wild Mongrel (gvl)]\n\n>>> only_bears = two_bears_One_mongrel - wild_mongrels\n>>> print(only_bears)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody)]\n\n>>> only_bears = two_bears_One_mongrel - wild_mongrels[0]\n>>> print(only_bears)\n\n[Werebear (ema), Werebear (td0), Werebear (wc02), Werebear (ody)]\n```\n\nCards in the lists can be multiplied. This is for example handy for getting playsets of certain cards (note, that\nyou have to multiply lists, not card objects):\n\n```\n>>> playset_of_bears = 4 * cards.where_exactly(name='Werebear')[0:1]\n>>> print(playset_of_bears)\n\n[Werebear (ema), Werebear (ema), Werebear (ema), Werebear (ema)]\n\n>>> bear_and_arena = PCardList() + cards.where_exactly(name='Werebear')[0] + cards.where_exactly(name='Arena')[0]\n>>> playset_of_bears_and_arenas = 4 * bear_and_arena\n>>> playset_of_bears_and_arenas.pprint()\n\nUnnamed card list created at 2018-07-18 14:51:23.759658\n------------------------------------------------------------------\nCard Set Type Cost Rarity\n------------------------------------------------------------------\n4 Arena tsb Land rare\n4 Werebear ema Creature - Human Bear Druid {1}{G} common\n```\n\n#### Working with multifaced cards from Scryfall\n\nAs mentioned earlier, some cards in Scryfall have multiple faces. These are for example flip-cards like\n'_Akki Lavarunner // Tok-Tok, Volcano Born_' and transform-cards like '_Accursed Witch // Infectious Curse_'. In some\ncases some attributes of these cards are only located inside the `card_faces` - dict attribute of the cards.\nIn other cases the card itself might have non-null attribute and the same attribute with a different value in one of\nit's card faces.\n\nFor example, the `name` attribute of the _'Akki Lavarunner'_ card object is '_Akki Lavarunner // Tok-Tok, Volcano Born_'\nthe `name` attribute of it's first card face is _'Akki Lavarunner'_ and the `name` attribute of it's second card face is\n'_Tok-Tok, Volcano Born_'. Similarly, the `mana_cost` - attribute of the card '_Accursed Witch // Infectious Curse_' is\nnull, but the the `mana_cost` - attribute of it's first card face is '_{3}{B}'_ which is what we would expect from this\ncard. For that reason, by default the first card face is also matched if it is non-null.\n\nIf you want to search all faces of the card you must set `search_all_faces=True` when querying. It might take some trial\nand error at first to get what you exactly want.\n\n```\n>>> multifaced_cards = 2 * cards.where_exactly(name='Akki Lavarunner // Tok-Tok, Volcano Born')[0:1]\n>>> multifaced_cards += 2 * cards.where_exactly(name='Accursed Witch // Infectious Curse')[0:1]\n>>> print(multifaced_cards.where(type_line='Enchantment'))\n\n[Accursed Witch // Infectious Curse (soi), Accursed Witch // Infectious Curse (soi)]\n\n>>> print(multifaced_cards.where(power='2', toughness='2', search_all_faces=True).where(colors='R'))\n\n[Akki Lavarunner // Tok-Tok, Volcano Born (chk), Akki Lavarunner // Tok-Tok, Volcano Born (chk)]\n```\n\n#### Grouping cards\n\nCards in the lists can be grouped in various ways like color, type or converted mana cost. Grouping\nreturns dicts with group identities as keys and cards lists corresponding the group as values. When grouping by color or\ncolor identity, the keys will always have an alphabetical order like 'BR' and 'GUW' instead of the normal\n'WUBRG' - order.\n\n```\n>>> some_cards = 2 * cards.where_exactly(name='Werebear')[:1] + 2 * cards.where_exactly(name='Firebolt')[:1]\n>>> some_cards += 2 * cards.where_exactly(name='Forest')[:1] + 2 * cards.where_exactly(name='Act of Treason')[:1]\n>>> csome_cards += 2 * cards.where_exactly(name='Bristling Boar')[:1] + 2 * cards.where_exactly(name='Cleansing Nova')[:1]\n\n>>> for (key, val) in some_cards.grouped_by_converted_mana_cost().items():\n print(key,':', val)\n\n0.0 : [Forest (pss3), Forest (pss3)]\n2.0 : [Werebear (ema), Werebear (ema)]\n3.0 : [Act of Treason (m19), Act of Treason (m19)]\n4.0 : [Arcades, the Strategist (m19), Arcades, the Strategist (m19), Bristling Boar (m19), Bristling Boar (m19)]\n5.0 : [Cleansing Nova (m19), Cleansing Nova (m19)]\n\n>>> for (key, val) in some_cards.grouped_by_color().items():\n print(key, ':', val)\n\n : [Forest (pss3), Forest (pss3)]\nGUW : [Arcades, the Strategist (m19), Arcades, the Strategist (m19)]\nW : [Cleansing Nova (m19), Cleansing Nova (m19)]\nG : [Werebear (ema), Werebear (ema), Bristling Boar (m19), Bristling Boar (m19)]\nR : [Act of Treason (m19), Act of Treason (m19)]\n```\n\nThe `grouped_by_id` - method is useful for fast retrieval of multiples of card objects from the list. Each different\ncard contains a unique `id` attribute by which the ones in the list can be quickly retrieved by grouping them.\n\nYou can also create a special kind of grouping, or a kind of an \"index\", of the cards in the list by using the method\n`create_id_index` which returns a persistent BTree object. In practice you can use BTrees quite like normal Python dict\nand these can also be handily saved in the database as indexes.\n\n#### Reading cards from files and strings\n\nCards can also be retrieved from the database (and from any other card lists) by reading them from list-like strings\nand text files by using the `PCardList` - methods `from_str` and `from_file` which build new card lists of the cards\nfound in the list. The accepted format of the strings and text files is similar to the standard Apperentice and MWS\ndeck lists:\n\n```\n//Creatures (8)\n1 Wild Mongrel [od]\n4 Aquamoeba (od)\n2 Werebear\nnoose constrictor\n\n//Enchantments (1)\n1 [ULG] rancor\n\n//Sideboard (1)\n\nSB: 2 Werebear\n...\n```\n\nComment lines can be specified with '//', possible desired sets can be specified with either '(set_code)'\nor '[set_code]' and sideboard cards with the prefix 'SB:'. The set brackets can be anywhere but the desired number\nof cards must come before the name of the card. If no matching set is found, a card from a random set is returned.\n\nNote, that this method is useful with the whole database of cards rather than a small list.\n\n```\n>>> my_deck1 = cards.from_str(\"\"\"\n3 Raging Ravine\n1 Wooded Foothills\n4 Verdant Catacombs\n1 Stomping Ground\n2 Overgrown Tomb\n1 Blood Crypt\n4 Blackcleave Cliffs\n2 Swamp\n1 Forest\n4 Bloodstained Mire\n\n2 Huntmaster of the Fells\n4 Dark Confidant\n2 Scavenging Ooze\n4 Tarmogoyf\n1 Fulminator Mage\n\n1 Chandra, Torch of Defiance\n4 Liliana of the Veil\n\n1 Kolaghan's Command\n4 Lightning Bolt\n3 Terminate\n3 Thoughtseize\n2 Abrupt Decay\n3 Inquisition of Kozilek\n\n1 Fatal Push\n\n1 Blooming Marsh\n1 Kalitas, Traitor of Ghet\n\n//Sideboard\nSB: 3 Fulminator Mage\nSB: 2 Collective Brutality\nSB: 1 Anger of the Gods\nSB: 1 Kolaghan's Command\nSB: 2 Ancient Grudge\nSB: 1 Maelstrom Pulse\nSB: 1 Liliana, the Last Hope\nSB: 2 Surgical Extraction\nSB: 1 Rakdos Charm\nSB: 1 Damnation\"\"\")\n\n>>> print(my_deck1.deck_str())\n\n// Lands (24)\n3 Raging Ravine [wwk]\n1 Wooded Foothills [g09]\n4 Verdant Catacombs [zen]\n1 Stomping Ground [exp]\n2 Overgrown Tomb [rtr]\n1 Blood Crypt [dis]\n4 Blackcleave Cliffs [som]\n2 Swamp [td0]\n1 Forest [ice]\n4 Bloodstained Mire [g09]\n1 Blooming Marsh [kld]\n\n// Creatures (14)\n4 Dark Confidant [mma]\n2 Scavenging Ooze [cmd]\n4 Tarmogoyf [mm3]\n1 Fulminator Mage [shm]\n2 Huntmaster of the Fells // Ravager of the Fells [dka]\n1 Kalitas, Traitor of Ghet [pogw]\n\n// Instants (11)\n1 Kolaghan's Command [pdtk]\n4 Lightning Bolt [mm2]\n3 Terminate [cmd]\n2 Abrupt Decay [prm]\n1 Fatal Push [f17]\n\n// Sorceries (6)\n3 Thoughtseize [ima]\n3 Inquisition of Kozilek [cn2]\n\n// Planeswalkers (5)\n4 Liliana of the Veil [isd]\n1 Chandra, Torch of Defiance [ps18]\n\n// Sideboard (15)\nSB:3 Fulminator Mage [mm2]\nSB:2 Collective Brutality [pemn]\nSB:1 Anger of the Gods [ima]\nSB:1 Kolaghan's Command [dtk]\nSB:2 Ancient Grudge [tsp]\nSB:1 Maelstrom Pulse [mma]\nSB:1 Liliana, the Last Hope [emn]\nSB:2 Surgical Extraction [prm]\nSB:1 Rakdos Charm [c17]\nSB:1 Damnation [prm]\n```\n\nYou can also structure the deck strings in different ways. For example by color and without set codes:\n\n```\n>>> print(my_deck.deck_str(group_by='color', add_set_codes=False))\n\n// Black (16)\n3 Inquisition of Kozilek\n1 Kalitas, Traitor of Ghet\n4 Liliana of the Veil\n3 Thoughtseize\n4 Dark Confidant\n1 Fatal Push\n\n// Colorless (24)\n2 Overgrown Tomb\n1 Wooded Foothills\n2 Swamp\n3 Raging Ravine\n1 Stomping Ground\n1 Blooming Marsh\n4 Blackcleave Cliffs\n4 Bloodstained Mire\n4 Verdant Catacombs\n1 Forest\n1 Blood Crypt\n\n// Red (5)\n1 Chandra, Torch of Defiance\n4 Lightning Bolt\n\n// Multicolor (9)\n2 Abrupt Decay\n1 Fulminator Mage\n2 Huntmaster of the Fells // Ravager of the Fells\n1 Kolaghan's Command\n3 Terminate\n\n// Green (6)\n4 Tarmogoyf\n2 Scavenging Ooze\n\n// Sideboard (15)\nSB: 2 Surgical Extraction\nSB: 1 Damnation\nSB: 1 Maelstrom Pulse\nSB: 1 Kolaghan's Command\nSB: 2 Collective Brutality\nSB: 1 Rakdos Charm\nSB: 1 Anger of the Gods\nSB: 2 Ancient Grudge\nSB: 3 Fulminator Mage\nSB: 1 Liliana, the Last Hope\n```\n\n`from_file` works exactly the same way except it reads the contents from a text file.\n\n#### More examples\n\nWe can use the looser `where` to look for cards with for example certain power or toughness by using the fact that\nfor numerical attributes it is enough for the argument to be equal or larger. Note, that for `power`, `toughness` and\n`loyalty` you can use the numerical versions `power_num`, `touhness_num` and `loyalty_num`.\n\nCreatures in Odyssey with power > 5:\n\n```\n>>> ody = sets.where_exactly(code='ody')[0]\n>>> ody.creatures().where(power_num=5, invert=True).pprint()\n\nUnnamed card list created at 2018-07-20 15:32:59.202900\n---------------------------------------------------------------------------------------\nCard Set Type Cost Rarity\n---------------------------------------------------------------------------------------\n1 Amugaba ody Creature - Illusion {5}{U}{U} rare\n1 Kamahl, Pit Fighter ody Legendary Creature - Human Barbarian {4}{R}{R} rare\n1 Ashen Firebeast ody Creature - Elemental Beast {6}{R}{R} rare\n```\n\nWhite creatures Not including multicolors in Odyssey with power <= 2 AND toughness <= 2:\n\n```\n>>> ody = sets.where_exactly(code='ody')[0]\n>>> ody.creatures().where(power_num=2).where(toughness_num=2).where_exactly(colors='W').pprint()\n\nUnnamed card list created at 2018-07-20 16:03:08.122375 with a total of 20 cards\n------------------------------------------------------------------------------------------\nCard Set Type Cost Rarity\n------------------------------------------------------------------------------------------\n1 Mystic Crusader ody Creature - Human Nomad Mystic {1}{W}{W} rare\n1 Beloved Chaplain ody Creature - Human Cleric {1}{W} uncommon\n1 Devoted Caretaker ody Creature - Human Cleric {W} rare\n1 Confessor ody Creature - Human Cleric {W} common\n1 Dogged Hunter ody Creature - Human Nomad {2}{W} rare\n1 Tireless Tribe ody Creature - Human Nomad {W} common\n1 Soulcatcher ody Creature - Bird Soldier {1}{W} uncommon\n1 Master Apothecary ody Creature - Human Cleric {W}{W}{W} rare\n1 Aven Archer ody Creature - Bird Soldier Archer {3}{W}{W} uncommon\n1 Lieutenant Kirtar ody Legendary Creature - Bird Soldier {1}{W}{W} rare\n1 Nomad Decoy ody Creature - Human Nomad {2}{W} uncommon\n1 Mystic Penitent ody Creature - Human Nomad Mystic {W} uncommon\n1 Cantivore ody Creature - Lhurgoyf {1}{W}{W} rare\n1 Aven Cloudchaser ody Creature - Bird Soldier {3}{W} common\n1 Hallowed Healer ody Creature - Human Cleric {2}{W} common\n1 Pianna, Nomad Captain ody Legendary Creature - Human Nomad {1}{W}{W} rare\n1 Auramancer ody Creature - Human Wizard {2}{W} common\n1 Mystic Visionary ody Creature - Human Nomad Mystic {1}{W} common\n1 Dedicated Martyr ody Creature - Human Cleric {W} common\n1 Patrol Hound ody Creature - Hound {1}{W} common\n```\n\nAuras in Odyssey with cmc <= 2:\n\n```\n>>> ody = sets.where_exactly(code='ody')[0]\n>>> ody.where(cmc=2).where(type_line='aura').pprint()\n\nUnnamed card list created at 2018-07-20 16:05:48.340844 with a total of 7 cards\n-------------------------------------------------------------------\nCard Set Type Cost Rarity\n-------------------------------------------------------------------\n1 Aboshan's Desire ody Enchantment - Aura {U} common\n1 Psionic Gift ody Enchantment - Aura {1}{U} common\n1 Primal Frenzy ody Enchantment - Aura {G} common\n1 Immobilizing Ink ody Enchantment - Aura {1}{U} common\n1 Druid's Call ody Enchantment - Aura {1}{G} uncommon\n1 Kirtar's Desire ody Enchantment - Aura {W} common\n1 Kamahl's Desire ody Enchantment - Aura {1}{R} common\n```\n\n### Saving your own things in the database\n\nA good guide about saving things in ZODB can be found here:\nhttp://www.zodb.org/en/latest/guide/writing-persistent-objects.html\n\nAny objects mentioned above are already Persistent, so they can be conveniently saved. For example any `PCardList`\nobjects can easily be saved with\n\n```\n>>> my_favourite_cards = cards.where_exactly(name='Counterspell', set='plgm') + cards.where_exactly(name='Cancel', set='p10')\n>>> my_favourite_cards.name = 'My fav cards'\n>>> mtg_db.root.my_favourite_cards = my_favourite_cards\n>>> mtg_db.commit()\n```\n\nYou can then easily later append more cards with\n\n```\n>>> mtg_db.root.my_favourite_cards.append(cards.where_exactly(name='Mana drain')[0])\n>>> mtg_db.commit()\n```\n\nand access them later with\n\n```\n>>> my_fav_cards = mtg_db.root.my_favourite_cards\n>>> my_fav_cards.pprint()\n\nCard list \"My fav cards\" created at 2018-07-18 18:36:54.135282\n-----------------------------------------------------\nCard Set Type Cost Rarity\n-----------------------------------------------------\n1 Counterspell plgm Instant {U}{U} rare\n1 Cancel p10 Instant {1}{U}{U} rare\n1 Mana Drain ima Instant {U}{U} mythic\n```\n\nYou can similarly save decks or other card lists for example by using a `PersistentList` which works almost like a\nnormal Python list:\n\n```\n>>> from persistent.list import PersistentList\n\n>>> my_deck1 = cards.from_str(\"\"\"\n3 Raging Ravine\n1 Wooded Foothills\n4 Verdant Catacombs\n1 Stomping Ground\n2 Overgrown Tomb\n1 Blood Crypt\n4 Blackcleave Cliffs\n2 Swamp\n1 Forest\n4 Bloodstained Mire\n\n2 Huntmaster of the Fells\n4 Dark Confidant\n2 Scavenging Ooze\n4 Tarmogoyf\n1 Fulminator Mage\n\n1 Chandra, Torch of Defiance\n4 Liliana of the Veil\n\n1 Kolaghan's Command\n4 Lightning Bolt\n3 Terminate\n3 Thoughtseize\n2 Abrupt Decay\n3 Inquisition of Kozilek\n\n1 Fatal Push\n\n1 Blooming Marsh\n1 Kalitas, Traitor of Ghet\n\n//Sideboard\nSB: 3 Fulminator Mage\nSB: 2 Collective Brutality\nSB: 1 Anger of the Gods\nSB: 1 Kolaghan's Command\nSB: 2 Ancient Grudge\nSB: 1 Maelstrom Pulse\nSB: 1 Liliana, the Last Hope\nSB: 2 Surgical Extraction\nSB: 1 Rakdos Charm\nSB: 1 Damnation\"\"\")\n\n>>> my_deck2 = cards.from_str(\"\"\"\n//Main\n4 Baral, Chief of Compliance\n4 Desperate Ritual\n4 Gifts Ungiven\n2 Goblin Electromancer\n3 Grapeshot\n4 Island\n4 Manamorphose\n1 Mountain\n1 Noxious Revival\n3 Opt\n2 Past in Flames\n4 Pyretic Ritual\n2 Remand\n1 Repeal\n4 Serum Visions\n4 Shivan Reef\n4 Sleight of Hand\n4 Spirebluff Canal\n4 Steam Vents\n1 Unsubstantiate\n\n//Sideboard\nSB: 1 Abrade\nSB: 1 Echoing Truth\nSB: 1 Empty the Warrens\nSB: 1 Gigadrowse\nSB: 3 Lightning Bolt\nSB: 4 Pieces of the Puzzle\nSB: 2 Pyromancer Ascension\nSB: 1 Shattering Spree\nSB: 1 Wipe Away\n\"\"\")\n>>> my_decks = PersistentList(my_deck1, my_deck2)\n>>> mtg_db.root.my_decks = my_decks\n>>> mtg_db.commit()\n```\n\nand then later on you can append more lists and access them the same way with single cards.\n\nAnother thing you might want to save in the database is for example an index of cards for faster retrieval. An 'index'\nin this case would be a fast persistent dict like BTree where the keys are some unique identifiers. For `PCardList`,\nthere already exists the method `create_id_index` which returns a `BTree` in which the cards are indexed by their unique\n'id' values and each id maps to a single card object found in the original list. This is handy if called on the whole\ndatabase and saved:\n\n```\n>>> my_card_index = cards.create_id_index()\n>>> mtg_db.root.my_card_index = my_card_index\n>>> mtg_db.commit()\n```\n\nNow single cards can be retrieved very speedily from the index by using their `id`'s:\n\n```\n>>> print(mtg_db.root.my_card_index['0a448077-3b1f-4efd-a606-e3ff40fe1621'])\n\nCounterspell (wc00)\n```\n\nSimilarly, the index can also be used to speedily check if some object exists in the database.\n\n`PCardList` objects have a similar unique id so they are also simple to index if needed.\n\n### Working with sets and set lists\n\n`PSet` and `PSetList` work very similarly to `PCard` and `PCardList`. The difference is that `PSet`\nis also a `PCardList` and contains a set of it's own characteristic Magic: The Gathering set attributes. `PSetList`\nobjects can be searched, filtered and sorted exactly like `PCardList` objects by using the methods `where`,\n`where_exactly`, `filtered` and `sorted` which similarly return new `PSetList` objects.\n\nThe sets are saved in the database as a `PSetList`.\n\n#### Some examples\n\nSets in Masques block:\n\n```\n>>> sets.where(block='Masques').pprint()\n\nUnnamed set list created at 2018-07-18 13:39:44.602675\n-----------------------------------------------------\nSet Code Block Type Cards\n-----------------------------------------------------\nProphecy pcy Masques expansion 143\nNemesis nem Masques expansion 143\nMercadian Masques mmq Masques expansion 350\n```\n\nAll the sets containing a Negate:\n\n```\n>> sets.filtered(lambda pset: any(pset.where_exactly(name='Negate'))).pprint()\n\nUnnamed set list created at 2018-07-18 13:39:52.329598\n---------------------------------------------------------------------------------\nSet Code Block Type Cards\n---------------------------------------------------------------------------------\nSignature Spellbook: Jace ss1 spellbook 8\nBattlebond bbd draft_innovation 256\nRivals of Ixalan rix Ixalan expansion 205\nAether Revolt aer Kaladesh expansion 194\nConspiracy: Take the Crown cn2 draft_innovation 222\nOath of the Gatewatch ogw Battle for Zendikar expansion 186\nMagic Origins ori core 288\nDragons of Tarkir dtk Khans of Tarkir expansion 264\nMagic 2015 m15 core 284\nMagic 2014 m14 core 249\nMagic 2013 m13 core 249\nMagic 2012 m12 core 249\nMagic 2011 m11 core 249\nDuels of the Planeswalkers dpa box 113\nMagic 2010 m10 core 249\nMagic Player Rewards 2009 p09 promo 9\nMorningtide mor Lorwyn expansion 150\nMagic Online Promos prm promo 1198\n```\n\nNormal standard-legal sets without promos:\n\n```\n>>> standard_sets = sets.where(set_type='promo', invert=True)\n>>> def standard_legal(pset):\n return len(pset) and len(pset) == len(\n pset.filtered(lambda card: card.legalities['standard'] == 'legal' or card.legalities['standard'] == 'banned')\n )\n\n>>> standard_sets.filtered(standard_legal).pprint()\n\nUnnamed set list created at 2018-07-23 12:33:31.340701\n--------------------------------------------------------\nSet Code Block Type Cards\n--------------------------------------------------------\nCore Set 2019 m19 core 314\nDominaria dom expansion 280\nRivals of Ixalan rix Ixalan expansion 205\nIxalan xln Ixalan expansion 289\nHour of Devastation hou Amonkhet expansion 209\nAmonkhet akh Amonkhet expansion 287\nWelcome Deck 2017 w17 starter 30\nAether Revolt aer Kaladesh expansion 194\nKaladesh kld Kaladesh expansion 274\n```\n\n### What else can you do with cards and sets?\n\nThe rest of the methods and functionalities are quite self explanatory and well documented, so they don't need further\nguidance. You can for example create random booster packs from card lists with `random_pack`, download images from\nScryfall with `download_images_from_scryfall`, create sheets of proxy images of cards from Scryfall with\n`create_proxies`, turn your lists back to JSON with `json` and many more things.\n\n## Notes and possible problems\n\n#### Possible bugs\n\nThe tools are somewhat decently tested for Scryfall data but some bugs and weird behavior are to be expected,\nespecially with some special cards.\n\nCurrently, the data from magicthegathering.io is not tested but it should still work quite like Scryfall data. If you\nare using mtgio, be mindful of the differences between them.\n\n#### Some things about the database\n\n- Be mindful when using multiple different storages and formatting/re-updating old ones when you have saved your own\n lists. If something goes wrong and the old objects in the base of the database get replaced by new objects, the old\n objects which have references in your own saved lists don't function in the same way anymore. This is because the newly\n created object instances are not equal to the old ones in the database even though they have the same id's. Make sure\n you back up the old databases before updates. At the moment there is no good support for recovery.\n\n- The first time you access any objects in the database after opening it takes a lot of time. This is because the data\n is not yet cached at that point. When the data is cached, the objects are actually retrieved from the cache without\n database interactions, which is fast.\n\n- The database can't be used from multiple threads by default. `Storage` and `DB` instances can, but to use transactions\n and object access you must use different connections and transaction managers for each thread.\n\n- Objects can be used as keys in a dictionary but it might be slow.\n\n- Some attributes of the objects like lists and dicts, (for example `colors` and `card_faces`) are not immutable.\n ZODB does not automatically recognize changes to these kinds of attributes. When changing values inside an object's\n mutable attributes, you must manually set the object's `_p_changed` attribute to `True` before calling `commit`. Often\n simpler way is to just use assignment or `setattr` instead of changing something inside the attribute. In this case,\n when the whole attribute is reassigned, ZODB will recognize this and changes are saved when committing.\n\n## Authors\n\n**Esko-Kalervo Salaka**\n\n## License\n\nCopyright \u00c2\u00a9 2018 Esko-Kalervo Salaka.\nAll rights reserved.\n\nZope Public License (ZPL) Version 2.1\n\nA copyright notice accompanies this license document that identifies the\ncopyright holders.\n\nThis license has been certified as open source. It has also been designated as\nGPL compatible by the Free Software Foundation (FSF).\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions in source code must retain the accompanying copyright\n notice, this list of conditions, and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the accompanying copyright\n notice, this list of conditions, and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n\n3. Names of the copyright holders must not be used to endorse or promote\n products derived from this software without prior written permission from the\n copyright holders.\n\n4. The right to distribute this software or to use it for any purpose does not\n give you the right to use Servicemarks (sm) or Trademarks (tm) of the\n copyright\n holders. Use of them is covered by separate agreement with the copyright\n holders.\n\n5. If any files are modified, you must cause the modified files to carry\n prominent notices stating that you changed the files and the date of any\n change.\n\nDisclaimer\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY EXPRESSED\nOR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO\nEVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n## Acknowledgments\n\nThis software uses ZODB, a native object database for Python, which is a\ncopyright \u00c2\u00a9 by Zope Foundation and Contributors.\n\nThis software uses Scryfall's rest-like API which is a copyright \u00c2\u00a9 by Scryfall LLC.\n\nThis software uses rest-like API of magicthegathering.io which is a copyright \u00c2\u00a9 by Andrew Backes.\n\nThis software uses the Python Imaging Library (PIL) which is a copyright \u00c2\u00a9 1997-2011 by Secret Labs AB and\ncopyright \u00c2\u00a9 1995-2011 by Fredrik Lundh\n\nAll the graphical and literal information and data related to Magic: The Gathering which can be handled with this\nsoftware, such as card information and card images, is copyright \u00c2\u00a9 of Wizards of the Coast LLC, a\nHasbro inc. subsidiary.\n\nThis software is in no way endorsed or promoted by Scryfall, Zope Foundation, magicthegathering.io or\nWizards of the Coast.\n\nThis software is free and is created for the purpose of creating new Magic: The Gathering content and software, and\njust for fun.\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/EskoSalaka/mtgtools", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "mtgtools", "package_url": "https://pypi.org/project/mtgtools/", "platform": "", "project_url": "https://pypi.org/project/mtgtools/", "project_urls": { "Homepage": "https://github.com/EskoSalaka/mtgtools" }, "release_url": "https://pypi.org/project/mtgtools/0.9.40/", "requires_dist": [ "ZODB", "requests" ], "requires_python": "", "summary": "Collection of tools for easy handling of Magic: The Gathering card and set data on your computer from Scryfall and/or magicthegathering.io.", "version": "0.9.40" }, "last_serial": 5406193, "releases": { "0.9": [ { "comment_text": "", "digests": { "md5": "2686263678bbd3778808e642e0b0fb0f", "sha256": "c393085763ad74d60ae16e3c0b9724203ac876748a4f35f05c1b76da7866536c" }, "downloads": -1, "filename": "mtgtools-0.9.tar.gz", "has_sig": false, "md5_digest": "2686263678bbd3778808e642e0b0fb0f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 66510, "upload_time": "2018-07-24T09:27:07", "url": "https://files.pythonhosted.org/packages/f5/c5/a2489c311000f9dbe239757fe08c39316c23c0afbd86a766d6f9b697e06d/mtgtools-0.9.tar.gz" } ], "0.9.1": [ { "comment_text": "", "digests": { "md5": "716d4635deb9393f4d25675af4ee5de8", "sha256": "fef8c02a0a0ada4499fee2676f93c334beec691b5758454ac9354ce8e1baa243" }, "downloads": -1, "filename": "mtgtools-0.9.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "716d4635deb9393f4d25675af4ee5de8", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 48794, "upload_time": "2018-12-20T10:54:27", "url": "https://files.pythonhosted.org/packages/c6/dc/ba1122eebf5822a6d63b4d97c920580bbac03bdb76e5214a35bff94d4809/mtgtools-0.9.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ce04476b2e556990eb66d38ffa259a85", "sha256": "5af7f9769cb5c48e69e4dfdf25c612bb33be9390ecac03caceea9f62ce275c60" }, "downloads": -1, "filename": "mtgtools-0.9.1.tar.gz", "has_sig": false, "md5_digest": "ce04476b2e556990eb66d38ffa259a85", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 67076, "upload_time": "2018-12-20T10:54:29", "url": "https://files.pythonhosted.org/packages/ef/63/869f9736eb7e0c702e0dfe266def81c335a96aba52c7026d4673a9af2dd1/mtgtools-0.9.1.tar.gz" } ], "0.9.21": [ { "comment_text": "", "digests": { "md5": "7f77b73b54c6411191f054f79711e2f7", "sha256": "8b02d4df682348f190c6482f73eef886f7d596400734de29200b4a51534523b5" }, "downloads": -1, "filename": "mtgtools-0.9.21-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "7f77b73b54c6411191f054f79711e2f7", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 50398, "upload_time": "2019-02-07T12:20:23", "url": "https://files.pythonhosted.org/packages/66/f7/02ee2878fff91908307afc606a48755f0662036046c9c92cec1b1ae967ad/mtgtools-0.9.21-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "95ebcc9eb919239c6620f91187093ac5", "sha256": "3de94408b29b136e41e96e5b2044cfae598eb5693ef170fd465dd900665a764c" }, "downloads": -1, "filename": "mtgtools-0.9.21.tar.gz", "has_sig": false, "md5_digest": "95ebcc9eb919239c6620f91187093ac5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 68250, "upload_time": "2019-02-07T12:20:24", "url": "https://files.pythonhosted.org/packages/5e/62/e8333d9677be280ee081f4cc238d455a3b567bd37b6584d92b7677bded07/mtgtools-0.9.21.tar.gz" } ], "0.9.30": [ { "comment_text": "", "digests": { "md5": "7293a43e6fdead7cb26eb613673a2536", "sha256": "368c7b4b41971b5e6dc65b71a85ed690ac8a55f8cf0a9d4ba4d1ad085a3c588d" }, "downloads": -1, "filename": "mtgtools-0.9.30-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "7293a43e6fdead7cb26eb613673a2536", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 50416, "upload_time": "2019-04-05T06:52:53", "url": "https://files.pythonhosted.org/packages/85/9c/aa900fa94f08fb9d55b7cd9c503c660903f21d4981b8fdc3f25f31261ca1/mtgtools-0.9.30-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3c87990dd1da1ed71f9716bfe9e9a275", "sha256": "93fc4c619b92555e870198ce336389906184c9151bb9490753ae9192a792c9c5" }, "downloads": -1, "filename": "mtgtools-0.9.30.tar.gz", "has_sig": false, "md5_digest": "3c87990dd1da1ed71f9716bfe9e9a275", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 68332, "upload_time": "2019-04-05T06:52:54", "url": "https://files.pythonhosted.org/packages/ad/ca/53355d2689f2fb0dae30b3591d824e817ec1e6b6cbc3ec0271777eced5c3/mtgtools-0.9.30.tar.gz" } ], "0.9.40": [ { "comment_text": "", "digests": { "md5": "6c53f7135b9cecbfc2c1cc3480bbfdd3", "sha256": "b7d5cab1435c372dabf0a962e177710c50c2f837067c7a3d09c3af4ab2f6a388" }, "downloads": -1, "filename": "mtgtools-0.9.40-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "6c53f7135b9cecbfc2c1cc3480bbfdd3", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 50763, "upload_time": "2019-06-16T11:17:32", "url": "https://files.pythonhosted.org/packages/a1/0d/7f978bfa4da41434efc84e935b2b8a248d6c8d5e723c3c04ab08471e4a7a/mtgtools-0.9.40-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a1e302d856e1621574fb6837fb316399", "sha256": "1f112b57366f4999cd25f4d83e9610bd9a42c207236d370967204f3e052de865" }, "downloads": -1, "filename": "mtgtools-0.9.40.tar.gz", "has_sig": false, "md5_digest": "a1e302d856e1621574fb6837fb316399", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 68738, "upload_time": "2019-06-16T11:17:34", "url": "https://files.pythonhosted.org/packages/5a/79/ba7a814ec87d72377deffd39ef5942b4212f0e15dbab5d28ce5a7af1ce20/mtgtools-0.9.40.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "6c53f7135b9cecbfc2c1cc3480bbfdd3", "sha256": "b7d5cab1435c372dabf0a962e177710c50c2f837067c7a3d09c3af4ab2f6a388" }, "downloads": -1, "filename": "mtgtools-0.9.40-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "6c53f7135b9cecbfc2c1cc3480bbfdd3", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 50763, "upload_time": "2019-06-16T11:17:32", "url": "https://files.pythonhosted.org/packages/a1/0d/7f978bfa4da41434efc84e935b2b8a248d6c8d5e723c3c04ab08471e4a7a/mtgtools-0.9.40-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a1e302d856e1621574fb6837fb316399", "sha256": "1f112b57366f4999cd25f4d83e9610bd9a42c207236d370967204f3e052de865" }, "downloads": -1, "filename": "mtgtools-0.9.40.tar.gz", "has_sig": false, "md5_digest": "a1e302d856e1621574fb6837fb316399", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 68738, "upload_time": "2019-06-16T11:17:34", "url": "https://files.pythonhosted.org/packages/5a/79/ba7a814ec87d72377deffd39ef5942b4212f0e15dbab5d28ce5a7af1ce20/mtgtools-0.9.40.tar.gz" } ] }