spells-mtg 0.10.11__tar.gz → 0.11.0__tar.gz
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_mtg-0.10.11 → spells_mtg-0.11.0}/PKG-INFO +1 -1
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/pyproject.toml +1 -1
- spells_mtg-0.11.0/spells/.ruff_cache/.gitignore +2 -0
- spells_mtg-0.11.0/spells/.ruff_cache/0.8.6/17785301476771359756 +0 -0
- spells_mtg-0.11.0/spells/.ruff_cache/CACHEDIR.TAG +1 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/cache.py +37 -0
- spells_mtg-0.11.0/spells/card_data_files.py +180 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/draft_data.py +48 -22
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/external.py +3 -2
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/LICENSE +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/README.md +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/__init__.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/cards.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/columns.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/config.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/enums.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/extension.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/filter.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/log.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/manifest.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/schema.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/spells/utils.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/tests/__init__.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/tests/filter_test.py +0 -0
- {spells_mtg-0.10.11 → spells_mtg-0.11.0}/tests/utils_test.py +0 -0
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Signature: 8a477f597d28d172789f06886806bc55
|
|
@@ -7,6 +7,7 @@ and groupbys.
|
|
|
7
7
|
Caches are cleared per-set when new files are downloaded.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import datetime as dt
|
|
10
11
|
from enum import StrEnum
|
|
11
12
|
import os
|
|
12
13
|
from pathlib import Path
|
|
@@ -31,6 +32,8 @@ class EventType(StrEnum):
|
|
|
31
32
|
class DataDir(StrEnum):
|
|
32
33
|
CACHE = "cache"
|
|
33
34
|
EXTERNAL = "external"
|
|
35
|
+
RATINGS = "ratings"
|
|
36
|
+
DECK_COLOR = "deck_color"
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
def spells_print(mode, content):
|
|
@@ -141,6 +144,8 @@ def data_dir_path(cache_dir: DataDir) -> str:
|
|
|
141
144
|
ext = {
|
|
142
145
|
DataDir.CACHE: "Cache" if is_win else "cache",
|
|
143
146
|
DataDir.EXTERNAL: "External" if is_win else "external",
|
|
147
|
+
DataDir.RATINGS: "Ratings" if is_win else "ratings",
|
|
148
|
+
DataDir.DECK_COLOR: "DeckColor" if is_win else "deck_color",
|
|
144
149
|
}[cache_dir]
|
|
145
150
|
|
|
146
151
|
data_dir = os.path.join(data_home(), ext)
|
|
@@ -163,6 +168,38 @@ def data_file_path(set_code, dataset_type: str, event_type=EventType.PREMIER):
|
|
|
163
168
|
)
|
|
164
169
|
|
|
165
170
|
|
|
171
|
+
def card_ratings_file_path(
|
|
172
|
+
set_code: str,
|
|
173
|
+
format: str,
|
|
174
|
+
user_group: str,
|
|
175
|
+
deck_color: str,
|
|
176
|
+
start_date: dt.date,
|
|
177
|
+
end_date: dt.date,
|
|
178
|
+
) -> tuple[str, str]:
|
|
179
|
+
return os.path.join(
|
|
180
|
+
data_dir_path(DataDir.RATINGS),
|
|
181
|
+
set_code,
|
|
182
|
+
), (
|
|
183
|
+
f"{format}_{user_group}_{deck_color}_{start_date.strftime('%Y-%m-%d')}"
|
|
184
|
+
f"_{end_date.strftime('%Y-%m-%d')}.json"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def deck_color_file_path(
|
|
188
|
+
set_code: str,
|
|
189
|
+
format: str,
|
|
190
|
+
user_group: str,
|
|
191
|
+
start_date: dt.date,
|
|
192
|
+
end_date: dt.date,
|
|
193
|
+
) -> tuple[str, str]:
|
|
194
|
+
return os.path.join(
|
|
195
|
+
data_dir_path(DataDir.DECK_COLOR),
|
|
196
|
+
set_code,
|
|
197
|
+
), (
|
|
198
|
+
f"{format}_{user_group}_{start_date.strftime('%Y-%m-%d')}"
|
|
199
|
+
f"_{end_date.strftime('%Y-%m-%d')}.json"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
166
203
|
def cache_dir_for_set(set_code: str) -> str:
|
|
167
204
|
return os.path.join(data_dir_path(DataDir.CACHE), set_code)
|
|
168
205
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import os
|
|
3
|
+
import wget
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from spells import cache
|
|
8
|
+
from spells.enums import ColName
|
|
9
|
+
|
|
10
|
+
RATINGS_TEMPLATE = (
|
|
11
|
+
"https://www.17lands.com/card_ratings/data?expansion={set_code}&format={format}"
|
|
12
|
+
"{user_group_param}{deck_color_param}&start_date={start_date_str}&end_date={end_date_str}"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
DECK_COLOR_DATA_TEMPLATE = (
|
|
16
|
+
"https://www.17lands.com/color_ratings/data?expansion={set_code}&event_type={format}"
|
|
17
|
+
"{user_group_param}&start_date={start_date_str}&end_date={end_date_str}&combine_splash=true"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
START_DATE_MAP = {
|
|
21
|
+
"DFT": dt.date(2025, 2, 11),
|
|
22
|
+
"FIN": dt.date(2025, 6, 10),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ratings_col_defs = {
|
|
26
|
+
ColName.NAME: pl.col("name"),
|
|
27
|
+
ColName.COLOR: pl.col("color"),
|
|
28
|
+
ColName.RARITY: pl.col("rarity"),
|
|
29
|
+
ColName.CARD_TYPE: pl.col("types"),
|
|
30
|
+
ColName.IMAGE_URL: pl.col("url"),
|
|
31
|
+
ColName.NUM_SEEN: pl.col("seen_count"),
|
|
32
|
+
ColName.LAST_SEEN: pl.col("seen_count") * pl.col("avg_seen"),
|
|
33
|
+
ColName.NUM_TAKEN: pl.col("pick_count"),
|
|
34
|
+
ColName.TAKEN_AT: pl.col("pick_count") * pl.col("avg_pick"),
|
|
35
|
+
ColName.DECK: pl.col("game_count"),
|
|
36
|
+
ColName.WON_DECK: pl.col("win_rate") * pl.col("game_count"),
|
|
37
|
+
ColName.SIDEBOARD: pl.col("pool_count") - pl.col("game_count"),
|
|
38
|
+
ColName.OPENING_HAND: pl.col("opening_hand_game_count"),
|
|
39
|
+
ColName.WON_OPENING_HAND: pl.col("opening_hand_game_count")
|
|
40
|
+
* pl.col("opening_hand_win_rate"),
|
|
41
|
+
ColName.DRAWN: pl.col("drawn_game_count"),
|
|
42
|
+
ColName.WON_DRAWN: pl.col("drawn_win_rate") * pl.col("drawn_game_count"),
|
|
43
|
+
ColName.NUM_GIH: pl.col("ever_drawn_game_count"),
|
|
44
|
+
ColName.NUM_GIH_WON: pl.col("ever_drawn_game_count")
|
|
45
|
+
* pl.col("ever_drawn_win_rate"),
|
|
46
|
+
ColName.NUM_GNS: pl.col("never_drawn_game_count"),
|
|
47
|
+
ColName.WON_NUM_GNS: pl.col("never_drawn_game_count")
|
|
48
|
+
* pl.col("never_drawn_win_rate"),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
deck_color_col_defs = {
|
|
52
|
+
ColName.MAIN_COLORS: pl.col("short_name"),
|
|
53
|
+
ColName.NUM_GAMES: pl.col("games"),
|
|
54
|
+
ColName.NUM_WON: pl.col("wins"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def deck_color_df(
|
|
59
|
+
set_code: str,
|
|
60
|
+
format: str = "PremierDraft",
|
|
61
|
+
player_cohort: str = "all",
|
|
62
|
+
start_date: dt.date | None = None,
|
|
63
|
+
end_date: dt.date | None = None,
|
|
64
|
+
):
|
|
65
|
+
if start_date is None:
|
|
66
|
+
start_date = START_DATE_MAP[set_code]
|
|
67
|
+
if end_date is None:
|
|
68
|
+
end_date = dt.date.today() - dt.timedelta(days=1)
|
|
69
|
+
|
|
70
|
+
target_dir, filename = cache.deck_color_file_path(
|
|
71
|
+
set_code,
|
|
72
|
+
format,
|
|
73
|
+
player_cohort,
|
|
74
|
+
start_date,
|
|
75
|
+
end_date,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not os.path.isdir(target_dir):
|
|
79
|
+
os.makedirs(target_dir)
|
|
80
|
+
|
|
81
|
+
deck_color_file_path = os.path.join(target_dir, filename)
|
|
82
|
+
|
|
83
|
+
if not os.path.isfile(deck_color_file_path):
|
|
84
|
+
user_group_param = (
|
|
85
|
+
"" if player_cohort == "all" else f"&user_group={player_cohort}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
url = DECK_COLOR_DATA_TEMPLATE.format(
|
|
89
|
+
set_code=set_code,
|
|
90
|
+
format=format,
|
|
91
|
+
user_group_param=user_group_param,
|
|
92
|
+
start_date_str=start_date.strftime("%Y-%m-%d"),
|
|
93
|
+
end_date_str=end_date.strftime("%Y-%m-%d"),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
wget.download(
|
|
97
|
+
url,
|
|
98
|
+
out=deck_color_file_path,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
df = (
|
|
102
|
+
pl.read_json(deck_color_file_path)
|
|
103
|
+
.filter(~pl.col("is_summary"))
|
|
104
|
+
.select(
|
|
105
|
+
[
|
|
106
|
+
pl.lit(set_code).alias(ColName.EXPANSION),
|
|
107
|
+
pl.lit(format).alias(ColName.EVENT_TYPE),
|
|
108
|
+
(pl.lit("Top") if player_cohort == "top" else pl.lit(None)).alias(
|
|
109
|
+
ColName.PLAYER_COHORT
|
|
110
|
+
),
|
|
111
|
+
*[val.alias(key) for key, val in deck_color_col_defs.items()],
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return df
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def base_ratings_df(
|
|
120
|
+
set_code: str,
|
|
121
|
+
format: str = "PremierDraft",
|
|
122
|
+
player_cohort: str = "all",
|
|
123
|
+
deck_color: str = "any",
|
|
124
|
+
start_date: dt.date | None = None,
|
|
125
|
+
end_date: dt.date | None = None,
|
|
126
|
+
) -> pl.DataFrame:
|
|
127
|
+
if start_date is None:
|
|
128
|
+
start_date = START_DATE_MAP[set_code]
|
|
129
|
+
if end_date is None:
|
|
130
|
+
end_date = dt.date.today() - dt.timedelta(days=1)
|
|
131
|
+
|
|
132
|
+
ratings_dir, filename = cache.card_ratings_file_path(
|
|
133
|
+
set_code,
|
|
134
|
+
format,
|
|
135
|
+
player_cohort,
|
|
136
|
+
deck_color,
|
|
137
|
+
start_date,
|
|
138
|
+
end_date,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if not os.path.isdir(ratings_dir):
|
|
142
|
+
os.makedirs(ratings_dir)
|
|
143
|
+
|
|
144
|
+
ratings_file_path = os.path.join(ratings_dir, filename)
|
|
145
|
+
|
|
146
|
+
if not os.path.isfile(ratings_file_path):
|
|
147
|
+
user_group_param = (
|
|
148
|
+
"" if player_cohort == "all" else f"&user_group={player_cohort}"
|
|
149
|
+
)
|
|
150
|
+
deck_color_param = "" if deck_color == "any" else f"&deck_colors={deck_color}"
|
|
151
|
+
|
|
152
|
+
url = RATINGS_TEMPLATE.format(
|
|
153
|
+
set_code=set_code,
|
|
154
|
+
format=format,
|
|
155
|
+
user_group_param=user_group_param,
|
|
156
|
+
deck_color_param=deck_color_param,
|
|
157
|
+
start_date_str=start_date.strftime("%Y-%m-%d"),
|
|
158
|
+
end_date_str=end_date.strftime("%Y-%m-%d"),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
wget.download(
|
|
162
|
+
url,
|
|
163
|
+
out=ratings_file_path,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
df = pl.read_json(ratings_file_path)
|
|
167
|
+
|
|
168
|
+
return df.select(
|
|
169
|
+
[
|
|
170
|
+
pl.lit(set_code).alias(ColName.EXPANSION),
|
|
171
|
+
pl.lit(format).alias(ColName.EVENT_TYPE),
|
|
172
|
+
(pl.lit("Top") if player_cohort == "top" else pl.lit(None)).alias(
|
|
173
|
+
ColName.PLAYER_COHORT
|
|
174
|
+
),
|
|
175
|
+
(pl.lit(deck_color) if deck_color != "any" else pl.lit(None)).alias(
|
|
176
|
+
ColName.MAIN_COLORS
|
|
177
|
+
),
|
|
178
|
+
*[val.alias(key) for key, val in ratings_col_defs.items()],
|
|
179
|
+
]
|
|
180
|
+
)
|
|
@@ -6,6 +6,8 @@ Aggregate dataframes containing raw counts are cached in the local file system
|
|
|
6
6
|
for performance.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import datetime
|
|
9
11
|
import functools
|
|
10
12
|
import hashlib
|
|
11
13
|
import re
|
|
@@ -23,9 +25,19 @@ from spells import manifest
|
|
|
23
25
|
from spells.columns import ColDef, ColSpec, get_specs
|
|
24
26
|
from spells.enums import View, ColName, ColType
|
|
25
27
|
from spells.log import make_verbose
|
|
28
|
+
from spells.card_data_files import base_ratings_df
|
|
26
29
|
|
|
27
30
|
DF = TypeVar("DF", pl.LazyFrame, pl.DataFrame)
|
|
28
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class CardDataFileSpec():
|
|
34
|
+
set_code: str
|
|
35
|
+
format: str = "PremierDraft"
|
|
36
|
+
player_cohort: str = "all"
|
|
37
|
+
deck_color: str = "any"
|
|
38
|
+
start_date: datetime.datetime | None = None
|
|
39
|
+
end_date: datetime.datetime | None = None
|
|
40
|
+
|
|
29
41
|
|
|
30
42
|
def _cache_key(args) -> str:
|
|
31
43
|
"""
|
|
@@ -476,6 +488,7 @@ def summon(
|
|
|
476
488
|
write_cache: bool = True,
|
|
477
489
|
card_context: pl.DataFrame | dict[str, Any] | None = None,
|
|
478
490
|
set_context: pl.DataFrame | dict[str, Any] | None = None,
|
|
491
|
+
cdfs: CardDataFileSpec | None = None,
|
|
479
492
|
) -> pl.DataFrame:
|
|
480
493
|
specs = get_specs()
|
|
481
494
|
|
|
@@ -518,30 +531,43 @@ def summon(
|
|
|
518
531
|
col_def_map = _hydrate_col_defs(code, specs, set_card_context, this_set_context)
|
|
519
532
|
m = manifest.create(col_def_map, columns, group_by, filter_spec)
|
|
520
533
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
(
|
|
534
|
+
if cdfs is None:
|
|
535
|
+
calc_fn = functools.partial(_base_agg_df, code, m, use_streaming=use_streaming)
|
|
536
|
+
agg_df = _fetch_or_cache(
|
|
537
|
+
calc_fn,
|
|
526
538
|
code,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if View.CARD in m.view_cols:
|
|
538
|
-
card_cols = m.view_cols[View.CARD].union({ColName.NAME})
|
|
539
|
-
fp = cache.data_file_path(code, View.CARD)
|
|
540
|
-
card_df = pl.read_parquet(fp)
|
|
541
|
-
select_df = _view_select(
|
|
542
|
-
card_df, card_cols, m.col_def_map, is_agg_view=False
|
|
539
|
+
(
|
|
540
|
+
code,
|
|
541
|
+
sorted(m.view_cols.get(View.DRAFT, set())),
|
|
542
|
+
sorted(m.view_cols.get(View.GAME, set())),
|
|
543
|
+
sorted(c.signature or "" for c in m.col_def_map.values()),
|
|
544
|
+
sorted(m.base_view_group_by),
|
|
545
|
+
filter_spec,
|
|
546
|
+
),
|
|
547
|
+
read_cache=read_cache,
|
|
548
|
+
write_cache=write_cache,
|
|
543
549
|
)
|
|
544
|
-
|
|
550
|
+
if View.CARD in m.view_cols:
|
|
551
|
+
card_cols = m.view_cols[View.CARD].union({ColName.NAME})
|
|
552
|
+
fp = cache.data_file_path(code, View.CARD)
|
|
553
|
+
card_df = pl.read_parquet(fp)
|
|
554
|
+
select_df = _view_select(
|
|
555
|
+
card_df, card_cols, m.col_def_map, is_agg_view=False
|
|
556
|
+
)
|
|
557
|
+
agg_df = agg_df.join(select_df, on="name", how="outer", coalesce=True)
|
|
558
|
+
else:
|
|
559
|
+
assert len(codes) == 1, "Only one set supported for loading from card data file"
|
|
560
|
+
assert codes[0] == cdfs.set_code, "Wrong set file specified"
|
|
561
|
+
assert cdfs.format == "PremierDraft", "Only PremierDraft supported"
|
|
562
|
+
agg_df = base_ratings_df(
|
|
563
|
+
set_code=cdfs.set_code,
|
|
564
|
+
format=cdfs.format,
|
|
565
|
+
player_cohort=cdfs.player_cohort,
|
|
566
|
+
deck_color=cdfs.deck_color,
|
|
567
|
+
start_date=cdfs.start_date,
|
|
568
|
+
end_date=cdfs.end_date,
|
|
569
|
+
)
|
|
570
|
+
|
|
545
571
|
concat_dfs.append(agg_df)
|
|
546
572
|
|
|
547
573
|
full_agg_df = pl.concat(concat_dfs, how="vertical")
|
|
@@ -30,7 +30,6 @@ RESOURCE_TEMPLATE = (
|
|
|
30
30
|
"https://17lands-public.s3.amazonaws.com/analysis_data/{dataset_type}_data/"
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
class FileFormat(StrEnum):
|
|
35
34
|
CSV = "csv"
|
|
36
35
|
PARQUET = "parquet"
|
|
@@ -121,7 +120,8 @@ def _context() -> int:
|
|
|
121
120
|
for code in all_sets:
|
|
122
121
|
write_card_file(code, force_download=True)
|
|
123
122
|
get_set_context(code, force_download=True)
|
|
124
|
-
return 0
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
125
|
|
|
126
126
|
def _refresh(set_code: str):
|
|
127
127
|
return _add(set_code, force_download=True)
|
|
@@ -359,4 +359,5 @@ def get_set_context(set_code: str, force_download=False) -> int:
|
|
|
359
359
|
context_df.write_parquet(context_fp)
|
|
360
360
|
|
|
361
361
|
cache.spells_print(mode, f"Wrote file {context_fp}")
|
|
362
|
+
|
|
362
363
|
return 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|