spells-mtg 0.9.0__tar.gz → 0.9.2__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spells-mtg
3
- Version: 0.9.0
3
+ Version: 0.9.2
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.9.0"
14
+ version = "0.9.2"
15
15
 
16
16
  [project.license]
17
17
  text = "MIT"
@@ -1,5 +1,8 @@
1
1
  from spells.columns import ColSpec
2
2
  from spells.enums import ColType, ColName
3
3
  from spells.draft_data import summon, view_select, get_names
4
+ from spells.log import setup_logging
5
+
6
+ setup_logging()
4
7
 
5
8
  __all__ = ["summon", "view_select", "get_names", "ColSpec", "ColType", "ColName"]
@@ -580,12 +580,5 @@ for item in ColName:
580
580
  assert item in _specs, f"column {item} enumerated but not specified"
581
581
 
582
582
 
583
- class GetSpecs:
584
- def __init__(self, spec_dict: dict[str, ColSpec]):
585
- self._specs = spec_dict
586
-
587
- def __call__(self):
588
- return dict(self._specs)
589
-
590
-
591
- get_specs = GetSpecs(_specs)
583
+ def get_specs():
584
+ return dict(_specs)
@@ -9,6 +9,7 @@ for performance.
9
9
  import functools
10
10
  import hashlib
11
11
  import re
12
+ import logging
12
13
  from inspect import signature
13
14
  import os
14
15
  from typing import Callable, TypeVar, Any
@@ -21,7 +22,7 @@ import spells.filter
21
22
  import spells.manifest
22
23
  from spells.columns import ColDef, ColSpec, get_specs
23
24
  from spells.enums import View, ColName, ColType
24
-
25
+ from spells.log import make_verbose
25
26
 
26
27
  DF = TypeVar("DF", pl.LazyFrame, pl.DataFrame)
27
28
 
@@ -83,7 +84,11 @@ def _get_card_context(
83
84
  card_df, frozenset(columns), col_def_map, is_agg_view=False
84
85
  ).to_dicts()
85
86
 
87
+ names = get_names(set_code)
86
88
  loaded_context = {row[ColName.NAME]: row for row in select_rows}
89
+
90
+ for name in names:
91
+ loaded_context[name] = loaded_context.get(name, {})
87
92
  else:
88
93
  names = get_names(set_code)
89
94
  loaded_context = {name: {} for name in names}
@@ -99,10 +104,7 @@ def _get_card_context(
99
104
 
100
105
  names = list(loaded_context.keys())
101
106
  for name in names:
102
- assert (
103
- name in card_context
104
- ), f"card_context must include a row for each card name. {name} missing."
105
- for col, value in card_context[name].items():
107
+ for col, value in card_context.get(name, {}).items():
106
108
  loaded_context[name][col] = value
107
109
 
108
110
  return loaded_context
@@ -129,46 +131,55 @@ def _determine_expression(
129
131
 
130
132
  if spec.col_type == ColType.NAME_SUM:
131
133
  if spec.expr is not None:
132
- assert isinstance(
133
- spec.expr, Callable
134
- ), f"NAME_SUM column {col} must have a callable `expr` accepting a `name` argument"
135
- unnamed_exprs = [
136
- spec.expr(**{"name": name, **seed_params(spec.expr)}) for name in names
137
- ]
138
-
139
- expr = tuple(
140
- map(
141
- lambda ex, name: ex.alias(f"{col}_{name}"),
142
- unnamed_exprs,
143
- names,
134
+ try:
135
+ assert isinstance(
136
+ spec.expr, Callable
137
+ ), f"NAME_SUM column {col} must have a callable `expr` accepting a `name` argument"
138
+ unnamed_exprs = [
139
+ spec.expr(**{"name": name, **seed_params(spec.expr)})
140
+ for name in names
141
+ ]
142
+
143
+ expr = tuple(
144
+ map(
145
+ lambda ex, name: ex.alias(f"{col}_{name}"),
146
+ unnamed_exprs,
147
+ names,
148
+ )
144
149
  )
145
- )
150
+ except KeyError:
151
+ expr = tuple(pl.lit(None).alias(f"{col}_{name}") for name in names)
146
152
  else:
147
153
  expr = tuple(map(lambda name: pl.col(f"{col}_{name}"), names))
148
154
 
149
155
  elif spec.expr is not None:
150
156
  if isinstance(spec.expr, Callable):
151
- assert (
152
- not spec.col_type == ColType.AGG
153
- ), f"AGG column {col} must be a pure spells expression"
154
- params = seed_params(spec.expr)
155
- if (
156
- spec.col_type in (ColType.PICK_SUM, ColType.CARD_ATTR)
157
- and "name" in signature(spec.expr).parameters
158
- ):
159
- condition_col = (
160
- ColName.PICK if spec.col_type == ColType.PICK_SUM else ColName.NAME
161
- )
162
- expr = pl.lit(None)
163
- for name in names:
164
- name_params = {"name": name, **params}
165
- expr = (
166
- pl.when(pl.col(condition_col) == name)
167
- .then(spec.expr(**name_params))
168
- .otherwise(expr)
157
+ try:
158
+ assert (
159
+ not spec.col_type == ColType.AGG
160
+ ), f"AGG column {col} must be a pure spells expression"
161
+ params = seed_params(spec.expr)
162
+ if (
163
+ spec.col_type in (ColType.PICK_SUM, ColType.CARD_ATTR)
164
+ and "name" in signature(spec.expr).parameters
165
+ ):
166
+ condition_col = (
167
+ ColName.PICK
168
+ if spec.col_type == ColType.PICK_SUM
169
+ else ColName.NAME
169
170
  )
170
- else:
171
- expr = spec.expr(**params)
171
+ expr = pl.lit(None)
172
+ for name in names:
173
+ name_params = {"name": name, **params}
174
+ expr = (
175
+ pl.when(pl.col(condition_col) == name)
176
+ .then(spec.expr(**name_params))
177
+ .otherwise(expr)
178
+ )
179
+ else:
180
+ expr = spec.expr(**params)
181
+ except KeyError:
182
+ expr = pl.lit(None)
172
183
  else:
173
184
  expr = spec.expr
174
185
  expr = expr.alias(col)
@@ -265,10 +276,7 @@ def _hydrate_col_defs(
265
276
  assert len(names) > 0, "there should be names"
266
277
  hydrated = {}
267
278
  for col, spec in specs.items():
268
- try:
269
- expr = _determine_expression(col, spec, names, card_context, set_context)
270
- except KeyError:
271
- continue
279
+ expr = _determine_expression(col, spec, names, card_context, set_context)
272
280
  dependencies = _infer_dependencies(col, expr, specs, names)
273
281
 
274
282
  sig_expr = expr if isinstance(expr, pl.Expr) else expr[0]
@@ -348,8 +356,10 @@ def _fetch_or_cache(
348
356
 
349
357
  if read_cache:
350
358
  if cache.cache_exists(set_code, key):
359
+ logging.debug(f"Cache {key} found")
351
360
  return cache.read_cache(set_code, key)
352
361
 
362
+ logging.debug(f"Cache not found, calculating with signature {cache_args}")
353
363
  df = calc_fn()
354
364
 
355
365
  if write_cache:
@@ -440,9 +450,11 @@ def _base_agg_df(
440
450
  else:
441
451
  joined_df = pl.concat(join_dfs, how="horizontal")
442
452
 
453
+ joined_df = joined_df.select(sorted(joined_df.schema.names()))
443
454
  return joined_df
444
455
 
445
456
 
457
+ @make_verbose
446
458
  def summon(
447
459
  set_code: str | list[str],
448
460
  columns: list[str] | None = None,
@@ -464,8 +476,10 @@ def summon(
464
476
  specs.update(ext)
465
477
 
466
478
  if isinstance(set_code, str):
467
- card_context = {set_code: card_context}
468
- set_context = {set_code: set_context}
479
+ if not (isinstance(card_context, dict) and set_code in card_context):
480
+ card_context = {set_code: card_context}
481
+ if not (isinstance(set_context, dict) and set_code in set_context):
482
+ set_context = {set_code: set_context}
469
483
  codes = [set_code]
470
484
  else:
471
485
  codes = set_code
@@ -476,6 +490,7 @@ def summon(
476
490
 
477
491
  concat_dfs = []
478
492
  for code in codes:
493
+ logging.debug(f"Calculating agg df for {code}")
479
494
  if isinstance(card_context, pl.DataFrame):
480
495
  set_card_context = card_context.filter(pl.col("expansion") == code)
481
496
  elif isinstance(card_context, dict):
@@ -19,7 +19,7 @@ def context_cols(attr, silent: bool = False) -> dict[str, ColSpec]:
19
19
  col_type=ColType.NAME_SUM,
20
20
  expr=(
21
21
  lambda name, card_context: pl.lit(None)
22
- if card_context[name][attr] is None
22
+ if card_context[name].get(attr) is None
23
23
  or math.isnan(card_context[name][attr])
24
24
  else pl.when(pl.col(f"pack_card_{name}") > 0)
25
25
  .then(card_context[name][attr])
@@ -29,7 +29,8 @@ def context_cols(attr, silent: bool = False) -> dict[str, ColSpec]:
29
29
  f"pick_{attr}_sum": ColSpec(
30
30
  col_type=ColType.PICK_SUM,
31
31
  expr=lambda name, card_context: pl.lit(None)
32
- if card_context[name][attr] is None or math.isnan(card_context[name][attr])
32
+ if card_context[name].get(attr) is None
33
+ or math.isnan(card_context[name][attr])
33
34
  else card_context[name][attr],
34
35
  ),
35
36
  f"pick_{attr}": ColSpec(
@@ -29,10 +29,12 @@ RESOURCE_TEMPLATE = (
29
29
  "https://17lands-public.s3.amazonaws.com/analysis_data/{dataset_type}_data/"
30
30
  )
31
31
 
32
+
32
33
  class FileFormat(StrEnum):
33
34
  CSV = "csv"
34
35
  PARQUET = "parquet"
35
36
 
37
+
36
38
  # Fred Cirera via https://stackoverflow.com/questions/1094841/get-a-human-readable-version-of-a-file-size
37
39
  def sizeof_fmt(num, suffix="B"):
38
40
  for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
@@ -98,7 +100,7 @@ def cli() -> int:
98
100
  def _add(set_code: str, force_download=False):
99
101
  download_data_set(set_code, View.DRAFT, force_download=force_download)
100
102
  write_card_file(set_code, force_download=force_download)
101
- get_set_context(set_code)
103
+ get_set_context(set_code, force_download=force_download)
102
104
  download_data_set(set_code, View.GAME, force_download=force_download)
103
105
  return 0
104
106
 
@@ -323,7 +325,11 @@ def get_set_context(set_code: str, force_download=False) -> int:
323
325
  )
324
326
  return 1
325
327
 
326
- df = summon(set_code, columns=[ColName.NUM_DRAFTS], group_by=[ColName.DRAFT_DATE, ColName.PICK_NUM])
328
+ df = summon(
329
+ set_code,
330
+ columns=[ColName.NUM_DRAFTS],
331
+ group_by=[ColName.DRAFT_DATE, ColName.PICK_NUM],
332
+ )
327
333
 
328
334
  context_df = df.filter(pl.col(ColName.NUM_DRAFTS) > 1000).select(
329
335
  [
@@ -0,0 +1,68 @@
1
+ from contextlib import contextmanager
2
+ import logging
3
+ import logging.handlers
4
+ from pathlib import Path
5
+ import sys
6
+ from typing import Callable
7
+
8
+ from spells.cache import data_home
9
+
10
+
11
+ def setup_logging(
12
+ log_level=logging.DEBUG,
13
+ log_file_name="spells.log",
14
+ max_bytes=5_000_000, # 5MB
15
+ backup_count=3,
16
+ log_format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
17
+ ):
18
+ log_dir = Path(data_home()) / ".logs"
19
+ log_dir.mkdir(parents=True, exist_ok=True)
20
+ log_file_path = log_dir / log_file_name
21
+
22
+ handler = logging.handlers.RotatingFileHandler(
23
+ filename=log_file_path, maxBytes=max_bytes, backupCount=backup_count
24
+ )
25
+
26
+ formatter = logging.Formatter(log_format)
27
+ handler.setFormatter(formatter)
28
+
29
+ root_logger = logging.getLogger()
30
+ root_logger.setLevel(log_level)
31
+
32
+ root_logger.handlers.clear()
33
+ root_logger.addHandler(handler)
34
+
35
+
36
+ def rotate_logs():
37
+ root_logger = logging.getLogger()
38
+ for handler in root_logger.handlers:
39
+ if isinstance(handler, logging.handlers.RotatingFileHandler):
40
+ handler.doRollover()
41
+ logging.debug("Log file manually rotated")
42
+
43
+
44
+ @contextmanager
45
+ def add_console_logging():
46
+ console_handler = logging.StreamHandler(sys.stdout)
47
+ formatter = logging.Formatter(
48
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
49
+ )
50
+ console_handler.setFormatter(formatter)
51
+ logger = logging.getLogger()
52
+ logger.addHandler(console_handler)
53
+
54
+ try:
55
+ yield
56
+ finally:
57
+ logger.removeHandler(console_handler)
58
+
59
+
60
+ def make_verbose(callable: Callable) -> Callable:
61
+ def wrapped(*args, verbose=False, **kwargs):
62
+ if verbose:
63
+ with add_console_logging():
64
+ return callable(*args, **kwargs)
65
+ else:
66
+ return callable(*args, **kwargs)
67
+
68
+ return wrapped
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