spells-mtg 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of spells-mtg might be problematic. Click here for more details.

spells/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from spells import columns
2
+ from spells import enums
3
+ from spells.draft_data import summon
4
+
5
+ __all__ = ["summon", "enums", "columns"]
spells/cache.py ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ Module for caching the result of distributed dataframe calculations to parquet files.
3
+
4
+ Caches are keyed by a hash that is function of set code, aggregation type, base filter,
5
+ and groupbys.
6
+
7
+ Caches are cleared per-set when new files are downloaded.
8
+ """
9
+
10
+ from enum import StrEnum
11
+ import os
12
+ import sys
13
+
14
+ import polars as pl
15
+
16
+
17
+ class DataDir(StrEnum):
18
+ CACHE = "cache"
19
+ EXTERNAL = "external"
20
+
21
+
22
+ def spells_print(mode, content):
23
+ print(f"🪄 {mode} ✨ {content}")
24
+
25
+
26
+ def data_home() -> str:
27
+ is_win = sys.platform == "win32"
28
+ return os.path.expanduser(
29
+ os.environ.get(
30
+ "SPELLS_DATA_HOME",
31
+ os.environ.get(
32
+ "XDG_DATA_HOME",
33
+ r"C:\Users\$USERNAME\AppData\Local\Spells"
34
+ if is_win
35
+ else "~/.local/share/spells/",
36
+ ),
37
+ )
38
+ )
39
+
40
+
41
+ def data_dir_path(cache_dir: DataDir) -> str:
42
+ """
43
+ Where 17Lands data is stored. MDU_DATA_DIR environment variable is used, if it exists,
44
+ otherwise the cwd is used
45
+ """
46
+ is_win = sys.platform == "win32"
47
+
48
+ ext = {
49
+ DataDir.CACHE: "Cache" if is_win else "cache",
50
+ DataDir.EXTERNAL: "External" if is_win else "external",
51
+ }[cache_dir]
52
+
53
+ data_dir = os.path.join(data_home(), ext)
54
+ return data_dir
55
+
56
+
57
+ def cache_dir_for_set(set_code: str) -> str:
58
+ return os.path.join(data_dir_path(DataDir.CACHE), set_code)
59
+
60
+
61
+ def cache_path_for_key(set_code: str, cache_key: str) -> str:
62
+ return os.path.join(cache_dir_for_set(set_code), cache_key + ".parquet")
63
+
64
+
65
+ def cache_exists(set_code: str, cache_key: str) -> bool:
66
+ return os.path.isdir(cache_dir_for_set(set_code)) and os.path.isfile(
67
+ cache_path_for_key(set_code, cache_key)
68
+ )
69
+
70
+
71
+ def read_cache(set_code: str, cache_key: str) -> pl.DataFrame:
72
+ return pl.read_parquet(cache_path_for_key(set_code, cache_key))
73
+
74
+
75
+ def write_cache(set_code: str, cache_key: str, df: pl.DataFrame) -> None:
76
+ cache_dir = cache_dir_for_set(set_code)
77
+ if not os.path.isdir(cache_dir):
78
+ os.makedirs(cache_dir)
79
+
80
+ df.write_parquet(cache_path_for_key(set_code, cache_key))
81
+
82
+
83
+ def clear(set_code: str) -> int:
84
+ mode = "clean"
85
+ cache_dir = cache_dir_for_set(set_code)
86
+ if os.path.isdir(cache_dir):
87
+ with os.scandir(cache_dir) as set_dir:
88
+ count = 0
89
+ for entry in set_dir:
90
+ if not entry.name.endswith(".parquet"):
91
+ spells_print(
92
+ mode,
93
+ f"Unexpected file {entry.name} found in local cache, please sort that out!",
94
+ )
95
+ return 1
96
+ count += 1
97
+ os.remove(entry)
98
+ spells_print(
99
+ mode, f"Removed {count} files from local cache for set {set_code}"
100
+ )
101
+ os.rmdir(cache_dir)
102
+ spells_print(mode, f"Removed local cache dir {cache_dir}")
103
+ return 0
104
+ else:
105
+ spells_print(mode, f"No local cache found for set {set_code}")
106
+ return 0
spells/cards.py ADDED
@@ -0,0 +1,96 @@
1
+ import json
2
+ import urllib.request
3
+ from enum import StrEnum
4
+
5
+ import polars as pl
6
+
7
+ from spells.enums import ColName
8
+
9
+
10
+ class CardAttr(StrEnum):
11
+ NAME = ColName.NAME
12
+ SET_CODE = ColName.SET_CODE
13
+ COLOR = ColName.COLOR
14
+ RARITY = ColName.RARITY
15
+ COLOR_IDENTITY = ColName.COLOR_IDENTITY
16
+ CARD_TYPE = ColName.CARD_TYPE
17
+ SUBTYPE = ColName.SUBTYPE
18
+ MANA_VALUE = ColName.MANA_VALUE
19
+ MANA_COST = ColName.MANA_COST
20
+ POWER = ColName.POWER
21
+ TOUGHNESS = ColName.TOUGHNESS
22
+ IS_BONUS_SHEET = ColName.IS_BONUS_SHEET
23
+ IS_DFC = ColName.IS_DFC
24
+
25
+
26
+ MTG_JSON_TEMPLATE = "https://mtgjson.com/api/v5/{set_code}.json"
27
+
28
+
29
+ def _fetch_mtg_json(set_code: str) -> dict:
30
+ request = urllib.request.Request(
31
+ MTG_JSON_TEMPLATE.format(set_code=set_code),
32
+ headers={"User-Agent": "spells-mtg/0.1.0"},
33
+ )
34
+
35
+ with urllib.request.urlopen(request) as f:
36
+ draft_set_json = json.loads(f.read().decode("utf-8"))
37
+
38
+ return draft_set_json
39
+
40
+
41
+ def _extract_value(set_code: str, name: str, card_dict: dict, field: CardAttr):
42
+ match field:
43
+ case CardAttr.NAME:
44
+ return name
45
+ case CardAttr.SET_CODE:
46
+ return card_dict.get("setCode", "")
47
+ case CardAttr.COLOR:
48
+ return "".join(card_dict.get("colors", []))
49
+ case CardAttr.RARITY:
50
+ return card_dict.get("rarity", "")
51
+ case CardAttr.COLOR_IDENTITY:
52
+ return "".join(card_dict.get("colorIdentity", []))
53
+ case CardAttr.CARD_TYPE:
54
+ return " ".join(card_dict.get("types", []))
55
+ case CardAttr.SUBTYPE:
56
+ return " ".join(card_dict.get("subtypes", []))
57
+ case CardAttr.MANA_VALUE:
58
+ return card_dict.get("manaValue", 0)
59
+ case CardAttr.MANA_COST:
60
+ return card_dict.get("manaCost", "")
61
+ case CardAttr.POWER:
62
+ return card_dict.get("power", None)
63
+ case CardAttr.TOUGHNESS:
64
+ return card_dict.get("toughness", None)
65
+ case CardAttr.IS_BONUS_SHEET:
66
+ return card_dict.get("setCode", set_code) != set_code
67
+ case CardAttr.IS_DFC:
68
+ return len(card_dict.get("otherFaceIds", [])) > 0
69
+
70
+
71
+ def card_df(draft_set_code: str, names: list[str]) -> pl.DataFrame:
72
+ draft_set_json = _fetch_mtg_json(draft_set_code)
73
+ set_codes = draft_set_json["data"]["booster"]["play"]["sourceSetCodes"]
74
+ set_codes.reverse()
75
+
76
+ card_data_map = {}
77
+ for set_code in set_codes:
78
+ if set_code != draft_set_code:
79
+ card_data = _fetch_mtg_json(set_code)["data"]["cards"]
80
+ else:
81
+ card_data = draft_set_json["data"]["cards"]
82
+
83
+ card_data.reverse() # prefer front face for split cards
84
+ face_name_cards = [item for item in card_data if "faceName" in item]
85
+ card_data_map.update({item["faceName"]: item for item in face_name_cards})
86
+ card_data_map.update({item["name"]: item for item in card_data})
87
+
88
+ return pl.DataFrame(
89
+ [
90
+ {
91
+ field: _extract_value(draft_set_code, name, card_data_map[name], field)
92
+ for field in CardAttr
93
+ }
94
+ for name in names
95
+ ]
96
+ )