spells-mtg 0.10.11__tar.gz → 0.11.1__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.

Files changed (25) hide show
  1. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/PKG-INFO +1 -1
  2. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/pyproject.toml +1 -1
  3. spells_mtg-0.11.1/spells/.ruff_cache/.gitignore +2 -0
  4. spells_mtg-0.11.1/spells/.ruff_cache/0.8.6/17785301476771359756 +0 -0
  5. spells_mtg-0.11.1/spells/.ruff_cache/CACHEDIR.TAG +1 -0
  6. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/cache.py +37 -0
  7. spells_mtg-0.11.1/spells/card_data_files.py +192 -0
  8. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/draft_data.py +48 -22
  9. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/external.py +3 -2
  10. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/LICENSE +0 -0
  11. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/README.md +0 -0
  12. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/__init__.py +0 -0
  13. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/cards.py +0 -0
  14. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/columns.py +0 -0
  15. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/config.py +0 -0
  16. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/enums.py +0 -0
  17. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/extension.py +0 -0
  18. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/filter.py +0 -0
  19. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/log.py +0 -0
  20. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/manifest.py +0 -0
  21. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/schema.py +0 -0
  22. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/spells/utils.py +0 -0
  23. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/tests/__init__.py +0 -0
  24. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/tests/filter_test.py +0 -0
  25. {spells_mtg-0.10.11 → spells_mtg-0.11.1}/tests/utils_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spells-mtg
3
- Version: 0.10.11
3
+ Version: 0.11.1
4
4
  Summary: analaysis of 17Lands.com public datasets
5
5
  Author-Email: Joel Barnes <oelarnes@gmail.com>
6
6
  License: MIT
@@ -11,7 +11,7 @@ dependencies = [
11
11
  ]
12
12
  requires-python = ">=3.11"
13
13
  readme = "README.md"
14
- version = "0.10.11"
14
+ version = "0.11.1"
15
15
 
16
16
  [project.license]
17
17
  text = "MIT"
@@ -0,0 +1,2 @@
1
+ # Automatically created by ruff.
2
+ *
@@ -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,192 @@
1
+ import datetime as dt
2
+ import os
3
+ import wget
4
+ from time import sleep
5
+
6
+ import polars as pl
7
+
8
+ from spells import cache
9
+ from spells.enums import ColName
10
+
11
+ RATINGS_TEMPLATE = (
12
+ "https://www.17lands.com/card_ratings/data?expansion={set_code}&format={format}"
13
+ "{user_group_param}{deck_color_param}&start_date={start_date_str}&end_date={end_date_str}"
14
+ )
15
+
16
+ DECK_COLOR_DATA_TEMPLATE = (
17
+ "https://www.17lands.com/color_ratings/data?expansion={set_code}&event_type={format}"
18
+ "{user_group_param}&start_date={start_date_str}&end_date={end_date_str}&combine_splash=true"
19
+ )
20
+
21
+ START_DATE_MAP = {
22
+ "DFT": dt.date(2025, 2, 11),
23
+ "TDM": dt.date(2025, 4, 8),
24
+ "FIN": dt.date(2025, 6, 10),
25
+ }
26
+
27
+ ratings_col_defs = {
28
+ ColName.NAME: pl.col("name"),
29
+ ColName.COLOR: pl.col("color"),
30
+ ColName.RARITY: pl.col("rarity"),
31
+ ColName.CARD_TYPE: pl.col("types"),
32
+ ColName.IMAGE_URL: pl.col("url"),
33
+ ColName.NUM_SEEN: pl.col("seen_count"),
34
+ ColName.LAST_SEEN: pl.col("seen_count") * pl.col("avg_seen"),
35
+ ColName.NUM_TAKEN: pl.col("pick_count"),
36
+ ColName.TAKEN_AT: pl.col("pick_count") * pl.col("avg_pick"),
37
+ ColName.DECK: pl.col("game_count"),
38
+ ColName.WON_DECK: pl.col("win_rate") * pl.col("game_count"),
39
+ ColName.SIDEBOARD: pl.col("pool_count") - pl.col("game_count"),
40
+ ColName.OPENING_HAND: pl.col("opening_hand_game_count"),
41
+ ColName.WON_OPENING_HAND: pl.col("opening_hand_game_count")
42
+ * pl.col("opening_hand_win_rate"),
43
+ ColName.DRAWN: pl.col("drawn_game_count"),
44
+ ColName.WON_DRAWN: pl.col("drawn_win_rate") * pl.col("drawn_game_count"),
45
+ ColName.NUM_GIH: pl.col("ever_drawn_game_count"),
46
+ ColName.NUM_GIH_WON: pl.col("ever_drawn_game_count")
47
+ * pl.col("ever_drawn_win_rate"),
48
+ ColName.NUM_GNS: pl.col("never_drawn_game_count"),
49
+ ColName.WON_NUM_GNS: pl.col("never_drawn_game_count")
50
+ * pl.col("never_drawn_win_rate"),
51
+ }
52
+
53
+ deck_color_col_defs = {
54
+ ColName.MAIN_COLORS: pl.col("short_name"),
55
+ ColName.NUM_GAMES: pl.col("games"),
56
+ ColName.NUM_WON: pl.col("wins"),
57
+ }
58
+
59
+
60
+ def deck_color_df(
61
+ set_code: str,
62
+ format: str = "PremierDraft",
63
+ player_cohort: str = "all",
64
+ start_date: dt.date | None = None,
65
+ end_date: dt.date | None = None,
66
+ ):
67
+ if start_date is None:
68
+ start_date = START_DATE_MAP[set_code]
69
+ if end_date is None:
70
+ end_date = dt.date.today() - dt.timedelta(days=1)
71
+
72
+ target_dir, filename = cache.deck_color_file_path(
73
+ set_code,
74
+ format,
75
+ player_cohort,
76
+ start_date,
77
+ end_date,
78
+ )
79
+
80
+ if not os.path.isdir(target_dir):
81
+ os.makedirs(target_dir)
82
+
83
+ deck_color_file_path = os.path.join(target_dir, filename)
84
+
85
+ if not os.path.isfile(deck_color_file_path):
86
+ user_group_param = (
87
+ "" if player_cohort == "all" else f"&user_group={player_cohort}"
88
+ )
89
+
90
+ url = DECK_COLOR_DATA_TEMPLATE.format(
91
+ set_code=set_code,
92
+ format=format,
93
+ user_group_param=user_group_param,
94
+ start_date_str=start_date.strftime("%Y-%m-%d"),
95
+ end_date_str=end_date.strftime("%Y-%m-%d"),
96
+ )
97
+
98
+ wget.download(
99
+ url,
100
+ out=deck_color_file_path,
101
+ )
102
+
103
+ df = (
104
+ pl.read_json(deck_color_file_path)
105
+ .filter(~pl.col("is_summary"))
106
+ .select(
107
+ [
108
+ pl.lit(set_code).alias(ColName.EXPANSION),
109
+ pl.lit(format).alias(ColName.EVENT_TYPE),
110
+ (pl.lit("Top") if player_cohort == "top" else pl.lit(None)).alias(
111
+ ColName.PLAYER_COHORT
112
+ ),
113
+ *[val.alias(key) for key, val in deck_color_col_defs.items()],
114
+ ]
115
+ )
116
+ )
117
+
118
+ return df
119
+
120
+
121
+ def base_ratings_df(
122
+ set_code: str,
123
+ format: str = "PremierDraft",
124
+ player_cohort: str = "all",
125
+ deck_colors: str | list[str] = "any",
126
+ start_date: dt.date | None = None,
127
+ end_date: dt.date | None = None,
128
+ ) -> pl.DataFrame:
129
+ if start_date is None:
130
+ start_date = START_DATE_MAP[set_code]
131
+ if end_date is None:
132
+ end_date = dt.date.today() - dt.timedelta(days=1)
133
+
134
+ if isinstance(deck_colors, str):
135
+ deck_colors = [deck_colors]
136
+
137
+ concat_list = []
138
+ for i, deck_color in enumerate(deck_colors):
139
+ ratings_dir, filename = cache.card_ratings_file_path(
140
+ set_code,
141
+ format,
142
+ player_cohort,
143
+ deck_color,
144
+ start_date,
145
+ end_date,
146
+ )
147
+
148
+ if not os.path.isdir(ratings_dir):
149
+ os.makedirs(ratings_dir)
150
+
151
+ ratings_file_path = os.path.join(ratings_dir, filename)
152
+
153
+ if not os.path.isfile(ratings_file_path):
154
+ if i > 0:
155
+ sleep(5)
156
+ user_group_param = (
157
+ "" if player_cohort == "all" else f"&user_group={player_cohort}"
158
+ )
159
+ deck_color_param = "" if deck_color == "any" else f"&deck_colors={deck_color}"
160
+
161
+ url = RATINGS_TEMPLATE.format(
162
+ set_code=set_code,
163
+ format=format,
164
+ user_group_param=user_group_param,
165
+ deck_color_param=deck_color_param,
166
+ start_date_str=start_date.strftime("%Y-%m-%d"),
167
+ end_date_str=end_date.strftime("%Y-%m-%d"),
168
+ )
169
+
170
+ wget.download(
171
+ url,
172
+ out=ratings_file_path,
173
+ )
174
+
175
+ concat_list.append(pl.read_json(ratings_file_path).with_columns(
176
+ (pl.lit(deck_color) if deck_color != "any" else pl.lit(None)).alias(
177
+ ColName.MAIN_COLORS
178
+ )
179
+ ))
180
+ df = pl.concat(concat_list)
181
+
182
+ return df.select(
183
+ [
184
+ pl.lit(set_code).alias(ColName.EXPANSION),
185
+ pl.lit(format).alias(ColName.EVENT_TYPE),
186
+ (pl.lit("Top") if player_cohort == "top" else pl.lit(None)).alias(
187
+ ColName.PLAYER_COHORT
188
+ ),
189
+ ColName.MAIN_COLORS,
190
+ *[val.alias(key) for key, val in ratings_col_defs.items()],
191
+ ]
192
+ )
@@ -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_colors: str = "any"
38
+ start_date: datetime.date | None = None
39
+ end_date: datetime.date | 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
- calc_fn = functools.partial(_base_agg_df, code, m, use_streaming=use_streaming)
522
- agg_df = _fetch_or_cache(
523
- calc_fn,
524
- code,
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
- sorted(m.view_cols.get(View.DRAFT, set())),
528
- sorted(m.view_cols.get(View.GAME, set())),
529
- sorted(c.signature or "" for c in m.col_def_map.values()),
530
- sorted(m.base_view_group_by),
531
- filter_spec,
532
- ),
533
- read_cache=read_cache,
534
- write_cache=write_cache,
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
- agg_df = agg_df.join(select_df, on="name", how="outer", coalesce=True)
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_colors=cdfs.deck_colors,
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