spells-mtg 0.9.0__tar.gz → 0.9.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.
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/PKG-INFO +1 -1
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/pyproject.toml +1 -1
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/__init__.py +3 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/columns.py +2 -9
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/draft_data.py +59 -44
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/extension.py +3 -2
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/external.py +7 -1
- spells_mtg-0.9.1/spells/log.py +68 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/LICENSE +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/README.md +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/cache.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/cards.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/config.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/enums.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/filter.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/manifest.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/spells/schema.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/tests/__init__.py +0 -0
- {spells_mtg-0.9.0 → spells_mtg-0.9.1}/tests/filter_test.py +0 -0
|
@@ -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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
468
|
-
|
|
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]
|
|
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]
|
|
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"):
|
|
@@ -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(
|
|
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(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
|
|
File without changes
|
|
File without changes
|