spells-mtg 0.10.10__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.

Files changed (25) hide show
  1. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/PKG-INFO +1 -1
  2. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/pyproject.toml +1 -1
  3. spells_mtg-0.11.0/spells/.ruff_cache/.gitignore +2 -0
  4. spells_mtg-0.11.0/spells/.ruff_cache/0.8.6/17785301476771359756 +0 -0
  5. spells_mtg-0.11.0/spells/.ruff_cache/CACHEDIR.TAG +1 -0
  6. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/cache.py +37 -0
  7. spells_mtg-0.11.0/spells/card_data_files.py +180 -0
  8. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/config.py +1 -0
  9. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/draft_data.py +48 -22
  10. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/external.py +3 -2
  11. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/manifest.py +5 -6
  12. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/LICENSE +0 -0
  13. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/README.md +0 -0
  14. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/__init__.py +0 -0
  15. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/cards.py +0 -0
  16. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/columns.py +0 -0
  17. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/enums.py +0 -0
  18. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/extension.py +0 -0
  19. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/filter.py +0 -0
  20. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/log.py +0 -0
  21. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/schema.py +0 -0
  22. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/spells/utils.py +0 -0
  23. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/tests/__init__.py +0 -0
  24. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/tests/filter_test.py +0 -0
  25. {spells_mtg-0.10.10 → spells_mtg-0.11.0}/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.10
3
+ Version: 0.11.0
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.10"
14
+ version = "0.11.0"
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,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
+ )
@@ -1,4 +1,5 @@
1
1
  all_sets = [
2
+ "TDM",
2
3
  "DFT",
3
4
  "PIO",
4
5
  "FDN",
@@ -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
- 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_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
@@ -1,9 +1,8 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- import spells.columns
4
- import spells.filter
3
+ import spells.filter as spells_filter
5
4
  from spells.enums import View, ColName, ColType
6
- from spells.columns import ColDef
5
+ from spells.columns import ColDef, default_columns
7
6
 
8
7
 
9
8
  @dataclass(frozen=True)
@@ -13,7 +12,7 @@ class Manifest:
13
12
  base_view_group_by: frozenset[str]
14
13
  view_cols: dict[View, frozenset[str]]
15
14
  group_by: tuple[str, ...]
16
- filter: spells.filter.Filter | None
15
+ filter: spells_filter.Filter | None
17
16
 
18
17
  def __post_init__(self):
19
18
  # No name filter check
@@ -166,7 +165,7 @@ def create(
166
165
  gbs = (ColName.NAME,) if group_by is None else tuple(group_by)
167
166
 
168
167
  if columns is None:
169
- cols = tuple(spells.columns.default_columns)
168
+ cols = tuple(default_columns)
170
169
  if ColName.NAME not in gbs:
171
170
  cols = tuple(
172
171
  col for col in cols if col not in (ColName.COLOR, ColName.RARITY)
@@ -174,7 +173,7 @@ def create(
174
173
  else:
175
174
  cols = tuple(columns)
176
175
 
177
- m_filter = spells.filter.from_spec(filter_spec)
176
+ m_filter = spells_filter.from_spec(filter_spec)
178
177
 
179
178
  col_set = frozenset(cols)
180
179
  col_set = col_set.union(frozenset(gbs) - {ColName.NAME})
File without changes
File without changes
File without changes