mtg-ssm 2.6.12.dev4__tar.gz → 2.7.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.
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/integration.yml +1 -1
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/pre-commit.yml +1 -1
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/publish.yml +1 -1
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/run-tests.yml +2 -2
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/PKG-INFO +11 -5
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/README.rst +6 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/_version.py +2 -2
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/bundles.py +7 -13
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/counts.py +4 -3
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/indexes.py +20 -19
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/legacy.py +7 -7
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/mtg/util.py +2 -3
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/scryfall/fetcher.py +2 -9
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/scryfall/models.py +24 -24
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/csv_serializer.py +7 -6
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/interface.py +9 -9
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/xlsx_serializer.py +9 -8
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/ssm.py +7 -7
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/PKG-INFO +11 -5
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/requires.txt +1 -1
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/pyproject.toml +6 -6
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/conftest.py +9 -9
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_counts.py +3 -3
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_legacy.py +4 -4
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/mtg/test_util.py +2 -3
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/scryfall/test_fetcher.py +6 -5
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/test_csv_serializer.py +1 -2
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/test_interface.py +1 -2
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/test_xlsx_serializer.py +5 -5
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.editorconfig +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/dependabot.yml +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.gitignore +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.pre-commit-config.yaml +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/LICENSE.txt +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/TODO.md +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/collection.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/mtg/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/py.typed +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/scryfall/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/SOURCES.txt +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/dependency_links.txt +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/entry_points.txt +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/top_level.txt +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/setup.cfg +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_bundles.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_indexes.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/LICENSE.txt +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/bulk_data.json +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/cards.json +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/migrations.json +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/sets.json +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/sets1.json +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/sets2.json +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/gen_testdata.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/mtg/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/scryfall/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__init__.py +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__snapshots__/test_csv_serializer.ambr +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__snapshots__/test_xlsx_serializer.ambr +0 -0
- {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/test_ssm.py +0 -0
|
@@ -11,11 +11,11 @@ jobs:
|
|
|
11
11
|
strategy:
|
|
12
12
|
matrix:
|
|
13
13
|
python-version:
|
|
14
|
-
- "3.8"
|
|
15
14
|
- "3.9"
|
|
16
15
|
- "3.10"
|
|
17
16
|
- "3.11"
|
|
18
17
|
- "3.12"
|
|
18
|
+
- "3.13"
|
|
19
19
|
os:
|
|
20
20
|
- ubuntu-latest
|
|
21
21
|
- macos-latest
|
|
@@ -44,11 +44,11 @@ jobs:
|
|
|
44
44
|
strategy:
|
|
45
45
|
matrix:
|
|
46
46
|
python-version:
|
|
47
|
-
- "3.8"
|
|
48
47
|
- "3.9"
|
|
49
48
|
- "3.10"
|
|
50
49
|
- "3.11"
|
|
51
50
|
- "3.12"
|
|
51
|
+
- "3.13"
|
|
52
52
|
name: Python ${{ matrix.python-version }} pytest
|
|
53
53
|
steps:
|
|
54
54
|
- uses: actions/checkout@v4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: mtg_ssm
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: A tool to manage Magic: the Gathering collection spreadsheets
|
|
5
5
|
Author-email: George Leslie-Waksman <waksman@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -19,18 +19,18 @@ Classifier: Operating System :: Unix
|
|
|
19
19
|
Classifier: Programming Language :: Python
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
27
|
Classifier: Topic :: Games/Entertainment
|
|
28
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
29
|
Description-Content-Type: text/x-rst
|
|
30
30
|
License-File: LICENSE.txt
|
|
31
31
|
Requires-Dist: appdirs~=1.4
|
|
32
32
|
Requires-Dist: msgspec~=0.15
|
|
33
|
-
Requires-Dist: openpyxl~=3.
|
|
33
|
+
Requires-Dist: openpyxl~=3.1
|
|
34
34
|
Requires-Dist: requests~=2.27
|
|
35
35
|
Requires-Dist: requests-cache~=0.9.8
|
|
36
36
|
Requires-Dist: typing_extensions~=4.8.0
|
|
@@ -240,6 +240,12 @@ Development
|
|
|
240
240
|
|
|
241
241
|
- ...
|
|
242
242
|
|
|
243
|
+
2.7.0
|
|
244
|
+
-----
|
|
245
|
+
|
|
246
|
+
- Drop support for Python 3.8
|
|
247
|
+
- Add support for Python 3.13
|
|
248
|
+
|
|
243
249
|
2.6.9
|
|
244
250
|
-----
|
|
245
251
|
|
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '2.
|
|
21
|
-
__version_tuple__ = version_tuple = (2,
|
|
20
|
+
__version__ = version = '2.7.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (2, 7, 0)
|
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
"""Data bundle definitions."""
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import NamedTuple, Optional
|
|
5
5
|
|
|
6
|
-
from mtg_ssm.scryfall.models import
|
|
7
|
-
ScryCard,
|
|
8
|
-
ScryCardLayout,
|
|
9
|
-
ScryMigration,
|
|
10
|
-
ScrySet,
|
|
11
|
-
ScrySetType,
|
|
12
|
-
)
|
|
6
|
+
from mtg_ssm.scryfall.models import ScryCard, ScryCardLayout, ScryMigration, ScrySet, ScrySetType
|
|
13
7
|
|
|
14
8
|
|
|
15
9
|
class ScryfallDataSet(NamedTuple):
|
|
16
10
|
"""Bundle for storing Scryfall data."""
|
|
17
11
|
|
|
18
|
-
sets:
|
|
19
|
-
cards:
|
|
20
|
-
migrations:
|
|
12
|
+
sets: list[ScrySet]
|
|
13
|
+
cards: list[ScryCard]
|
|
14
|
+
migrations: list[ScryMigration]
|
|
21
15
|
|
|
22
16
|
|
|
23
17
|
def filter_cards_and_sets( # noqa: C901
|
|
24
18
|
scryfall_data: ScryfallDataSet,
|
|
25
19
|
*,
|
|
26
|
-
exclude_set_types: Optional[
|
|
27
|
-
exclude_card_layouts: Optional[
|
|
20
|
+
exclude_set_types: Optional[set[ScrySetType]] = None,
|
|
21
|
+
exclude_card_layouts: Optional[set[ScryCardLayout]] = None,
|
|
28
22
|
exclude_digital: bool = False,
|
|
29
23
|
exclude_foreing_only: bool = False,
|
|
30
24
|
merge_promos: bool = False,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import collections
|
|
4
4
|
import enum
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Iterable, MutableMapping
|
|
6
|
+
from typing import Any
|
|
6
7
|
from uuid import UUID
|
|
7
8
|
|
|
8
9
|
from mtg_ssm.containers import legacy
|
|
@@ -24,12 +25,12 @@ class CountType(str, enum.Enum):
|
|
|
24
25
|
FOIL = "foil"
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
ScryfallCardCount =
|
|
28
|
+
ScryfallCardCount = dict[UUID, MutableMapping[CountType, int]]
|
|
28
29
|
"""Mapping from scryfall id to card printing type to count."""
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def aggregate_card_counts(
|
|
32
|
-
card_rows: Iterable[
|
|
33
|
+
card_rows: Iterable[dict[str, Any]], oracle: Oracle
|
|
33
34
|
) -> ScryfallCardCount:
|
|
34
35
|
"""Extract card counts from card rows."""
|
|
35
36
|
card_counts: ScryfallCardCount = {}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import collections
|
|
4
4
|
import string
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Optional
|
|
6
7
|
from uuid import UUID
|
|
7
8
|
|
|
8
9
|
from mtg_ssm.containers.bundles import ScryfallDataSet
|
|
@@ -10,21 +11,21 @@ from mtg_ssm.mtg import util
|
|
|
10
11
|
from mtg_ssm.scryfall.models import ScryCard, ScryMigrationStrategy, ScrySet
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def name_card_sort_key(card: ScryCard) ->
|
|
14
|
+
def name_card_sort_key(card: ScryCard) -> tuple[str, str, int, str]:
|
|
14
15
|
"""Key function for sorting cards in a by-name list."""
|
|
15
16
|
return (card.set, *util.collector_number_parts(card.collector_number))
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def set_card_sort_key(card: ScryCard) ->
|
|
19
|
+
def set_card_sort_key(card: ScryCard) -> tuple[str, int, str]:
|
|
19
20
|
"""Key function for sorting cards in a by-set list."""
|
|
20
21
|
return util.collector_number_parts(card.collector_number)
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def build_snnmas(
|
|
24
25
|
card: ScryCard,
|
|
25
|
-
) -> Iterable[
|
|
26
|
+
) -> Iterable[tuple[Optional[str], str, Optional[str], Optional[int], Optional[str]]]:
|
|
26
27
|
"""Build set, name, number, multiverse id tuple keys."""
|
|
27
|
-
names_cnums:
|
|
28
|
+
names_cnums: set[tuple[str, Optional[str]]] = {(card.name, card.collector_number)}
|
|
28
29
|
for i, card_face in enumerate(card.card_faces or ()):
|
|
29
30
|
names_cnums |= {
|
|
30
31
|
(card_face.name, card.collector_number),
|
|
@@ -32,11 +33,11 @@ def build_snnmas(
|
|
|
32
33
|
}
|
|
33
34
|
names_cnums |= {(n, None) for n, _ in names_cnums}
|
|
34
35
|
|
|
35
|
-
sets:
|
|
36
|
+
sets: set[Optional[str]] = {card.set, None}
|
|
36
37
|
|
|
37
|
-
mvids:
|
|
38
|
+
mvids: set[Optional[int]] = {None} | set(card.multiverse_ids or ())
|
|
38
39
|
|
|
39
|
-
artists:
|
|
40
|
+
artists: set[Optional[str]] = {card.artist, None}
|
|
40
41
|
|
|
41
42
|
for name, number in names_cnums:
|
|
42
43
|
for set_ in sets:
|
|
@@ -49,18 +50,18 @@ class ScryfallDataIndex:
|
|
|
49
50
|
"""Card and set indexes for scryfall data."""
|
|
50
51
|
|
|
51
52
|
def __init__(self) -> None:
|
|
52
|
-
self.id_to_card:
|
|
53
|
-
self.name_to_cards:
|
|
54
|
-
self.setcode_to_cards:
|
|
55
|
-
self.id_to_setindex:
|
|
56
|
-
self.setcode_to_set:
|
|
57
|
-
self.migrate_old_id_to_new_id:
|
|
53
|
+
self.id_to_card: dict[UUID, ScryCard] = {}
|
|
54
|
+
self.name_to_cards: dict[str, list[ScryCard]] = {}
|
|
55
|
+
self.setcode_to_cards: dict[str, list[ScryCard]] = {}
|
|
56
|
+
self.id_to_setindex: dict[UUID, int] = {}
|
|
57
|
+
self.setcode_to_set: dict[str, ScrySet] = {}
|
|
58
|
+
self.migrate_old_id_to_new_id: dict[UUID, UUID] = {}
|
|
58
59
|
# snnma = Set, Name, (Collector) Number, Multiverse ID, Artist
|
|
59
60
|
# TODO: convert to intersecting bitmap indexes
|
|
60
61
|
# TODO: do we really need artist?
|
|
61
|
-
self.snnma_to_id:
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
self.snnma_to_id: dict[
|
|
63
|
+
tuple[Optional[str], str, Optional[str], Optional[int], Optional[str]],
|
|
64
|
+
set[UUID],
|
|
64
65
|
] = {}
|
|
65
66
|
|
|
66
67
|
def load_data(self, scrydata: ScryfallDataSet) -> None:
|
|
@@ -71,8 +72,8 @@ class ScryfallDataIndex:
|
|
|
71
72
|
|
|
72
73
|
self.snnma_to_id = collections.defaultdict(set)
|
|
73
74
|
|
|
74
|
-
name_to_unsorted_cards:
|
|
75
|
-
setcode_to_unsorted_cards:
|
|
75
|
+
name_to_unsorted_cards: dict[str, list[ScryCard]] = collections.defaultdict(list)
|
|
76
|
+
setcode_to_unsorted_cards: dict[str, list[ScryCard]] = {}
|
|
76
77
|
|
|
77
78
|
for set_ in scrydata.sets:
|
|
78
79
|
self.setcode_to_set[set_.code] = set_
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Legacy record lookup capabilities for older file versions."""
|
|
2
2
|
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, Optional
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
from mtg_ssm.containers.indexes import Oracle
|
|
@@ -19,13 +19,13 @@ class MultipleMatchError(Error):
|
|
|
19
19
|
"""Raised when multiple matching cards are found."""
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
COUNT_TYPE_TO_OLD_COUNT_TYPES:
|
|
22
|
+
COUNT_TYPE_TO_OLD_COUNT_TYPES: dict[str, set[str]] = {
|
|
23
23
|
"nonfoil": {"nonfoil", "copies"},
|
|
24
24
|
"foil": {"foil", "foils"},
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def extract_counts(card_row:
|
|
28
|
+
def extract_counts(card_row: dict[str, Any]) -> dict[str, int]:
|
|
29
29
|
"""Convert old count type names to new count type names."""
|
|
30
30
|
renamed_counts = {
|
|
31
31
|
k: sum(int(card_row.get(t) or 0) for t in v)
|
|
@@ -93,13 +93,13 @@ OTHER_SET_CODE_TO_SET_CODE = {
|
|
|
93
93
|
"PCA": ["opca"],
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
PSUDONYM_TO_ARTIST:
|
|
96
|
+
PSUDONYM_TO_ARTIST: dict[Optional[str], str] = {
|
|
97
97
|
"William Murai": "Willian Murai",
|
|
98
98
|
"Dave Seeley": "David Seeley",
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def find_scryfall_id(card_row:
|
|
102
|
+
def find_scryfall_id(card_row: dict[str, str], oracle: Oracle) -> UUID:
|
|
103
103
|
"""Heuristically determine the scryfall id for a given input row."""
|
|
104
104
|
set_code = card_row.get("set", "")
|
|
105
105
|
set_codes = [
|
|
@@ -113,7 +113,7 @@ def find_scryfall_id(card_row: Dict[str, str], oracle: Oracle) -> UUID:
|
|
|
113
113
|
artist = card_row.get("artist") or None
|
|
114
114
|
artist = PSUDONYM_TO_ARTIST.get(artist, artist)
|
|
115
115
|
print(f"Searching => Set: {set_code}; Name: {name}; Number: {collector_number}; MVID: {mvid}")
|
|
116
|
-
snnma_keys:
|
|
116
|
+
snnma_keys: list[tuple[Optional[str], str, Optional[str], Optional[int], Optional[str]]] = []
|
|
117
117
|
for set_ in set_codes:
|
|
118
118
|
snnma_keys += [
|
|
119
119
|
(set_, name, collector_number, None, None),
|
|
@@ -145,7 +145,7 @@ def find_scryfall_id(card_row: Dict[str, str], oracle: Oracle) -> UUID:
|
|
|
145
145
|
raise NoMatchError(msg)
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def coerce_row(card_row:
|
|
148
|
+
def coerce_row(card_row: dict[str, Any], oracle: Oracle) -> dict[str, Any]:
|
|
149
149
|
"""Coerce an unknown older row into a current row.
|
|
150
150
|
|
|
151
151
|
If this cannot be done precisely, a warning will be displayed.
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
4
|
import re
|
|
5
|
-
from typing import Tuple
|
|
6
5
|
|
|
7
6
|
STRICT_BASICS = frozenset({"Plains", "Island", "Swamp", "Mountain", "Forest"})
|
|
8
7
|
|
|
9
8
|
COLLECTOR_NUMBER_RE = re.compile(r"(?P<prefix>.*-|\D+)?(?P<number>\d+)?(?P<suffix>\D.*)?")
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@functools.
|
|
13
|
-
def collector_number_parts(collector_number: str) ->
|
|
11
|
+
@functools.cache
|
|
12
|
+
def collector_number_parts(collector_number: str) -> tuple[str, int, str]:
|
|
14
13
|
"""Split a collector number into its parts."""
|
|
15
14
|
match = COLLECTOR_NUMBER_RE.match(collector_number)
|
|
16
15
|
if not match:
|
|
@@ -2,20 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import gzip
|
|
4
4
|
import os
|
|
5
|
-
from typing import List
|
|
6
5
|
|
|
7
6
|
import appdirs
|
|
8
7
|
import msgspec
|
|
9
8
|
from requests_cache import CachedSession, SerializerPipeline, Stage, pickle_serializer
|
|
10
9
|
|
|
11
10
|
from mtg_ssm.containers.bundles import ScryfallDataSet
|
|
12
|
-
from mtg_ssm.scryfall.models import
|
|
13
|
-
ScryBulkData,
|
|
14
|
-
ScryCard,
|
|
15
|
-
ScryList,
|
|
16
|
-
ScryMigration,
|
|
17
|
-
ScrySet,
|
|
18
|
-
)
|
|
11
|
+
from mtg_ssm.scryfall.models import ScryBulkData, ScryCard, ScryList, ScryMigration, ScrySet
|
|
19
12
|
|
|
20
13
|
APP_AUTHOR = "gwax"
|
|
21
14
|
APP_NAME = "mtg_ssm"
|
|
@@ -75,6 +68,6 @@ def scryfetch() -> ScryfallDataSet:
|
|
|
75
68
|
migrations_data += migrations_list.data
|
|
76
69
|
|
|
77
70
|
[cards_endpoint] = [bd.download_uri for bd in bulk_data if bd.type == BULK_TYPE]
|
|
78
|
-
cards_data = msgspec.json.decode(_fetch_endpoint(cards_endpoint), type=
|
|
71
|
+
cards_data = msgspec.json.decode(_fetch_endpoint(cards_endpoint), type=list[ScryCard])
|
|
79
72
|
|
|
80
73
|
return ScryfallDataSet(sets=sets_data, cards=cards_data, migrations=migrations_data)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import datetime as dt
|
|
6
6
|
from decimal import Decimal
|
|
7
7
|
from enum import Enum
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Generic, Optional, TypeVar, Union
|
|
9
9
|
from uuid import UUID
|
|
10
10
|
|
|
11
11
|
from msgspec import Struct
|
|
@@ -299,12 +299,12 @@ class ScryCardFace(
|
|
|
299
299
|
artist: Optional[str] = None
|
|
300
300
|
artist_id: Optional[UUID] = None
|
|
301
301
|
cmc: Optional[float] = None
|
|
302
|
-
color_indicator: Optional[
|
|
303
|
-
colors: Optional[
|
|
302
|
+
color_indicator: Optional[list[ScryColor]] = None
|
|
303
|
+
colors: Optional[list[ScryColor]] = None
|
|
304
304
|
flavor_name: Optional[str] = None
|
|
305
305
|
flavor_text: Optional[str] = None
|
|
306
306
|
illustration_id: Optional[UUID] = None
|
|
307
|
-
image_uris: Optional[
|
|
307
|
+
image_uris: Optional[dict[str, str]] = None
|
|
308
308
|
layout: Optional[ScryCardLayout] = None
|
|
309
309
|
loyalty: Optional[str] = None
|
|
310
310
|
mana_cost: str
|
|
@@ -343,7 +343,7 @@ class ScryCard(
|
|
|
343
343
|
lang: str
|
|
344
344
|
mtgo_id: Optional[int] = None
|
|
345
345
|
mtgo_foil_id: Optional[int] = None
|
|
346
|
-
multiverse_ids: Optional[
|
|
346
|
+
multiverse_ids: Optional[list[int]] = None
|
|
347
347
|
tcgplayer_id: Optional[int] = None
|
|
348
348
|
tcgplayer_etched_id: Optional[int] = None
|
|
349
349
|
cardmarket_id: Optional[int] = None
|
|
@@ -353,18 +353,18 @@ class ScryCard(
|
|
|
353
353
|
scryfall_uri: str
|
|
354
354
|
uri: str
|
|
355
355
|
# Gameplay Fields
|
|
356
|
-
all_parts: Optional[
|
|
357
|
-
card_faces: Optional[
|
|
356
|
+
all_parts: Optional[list[ScryRelatedCard]] = None
|
|
357
|
+
card_faces: Optional[list[ScryCardFace]] = None
|
|
358
358
|
cmc: Optional[float] = None
|
|
359
|
-
colors: Optional[
|
|
360
|
-
color_identity:
|
|
361
|
-
color_indicator: Optional[
|
|
359
|
+
colors: Optional[list[ScryColor]] = None
|
|
360
|
+
color_identity: list[ScryColor]
|
|
361
|
+
color_indicator: Optional[list[ScryColor]] = None
|
|
362
362
|
edhrec_rank: Optional[int] = None
|
|
363
363
|
foil: bool
|
|
364
364
|
hand_modifier: Optional[str] = None
|
|
365
|
-
keywords:
|
|
365
|
+
keywords: list[str]
|
|
366
366
|
layout: ScryCardLayout
|
|
367
|
-
legalities:
|
|
367
|
+
legalities: dict[ScryFormat, ScryLegality]
|
|
368
368
|
life_modifier: Optional[str] = None
|
|
369
369
|
loyalty: Optional[str] = None
|
|
370
370
|
mana_cost: Optional[str] = None
|
|
@@ -374,40 +374,40 @@ class ScryCard(
|
|
|
374
374
|
oversized: bool
|
|
375
375
|
penny_rank: Optional[int] = None
|
|
376
376
|
power: Optional[str] = None
|
|
377
|
-
produced_mana: Optional[
|
|
377
|
+
produced_mana: Optional[list[str]] = None
|
|
378
378
|
reserved: bool
|
|
379
379
|
toughness: Optional[str] = None
|
|
380
380
|
type_line: Optional[str] = None
|
|
381
381
|
# Print Fields
|
|
382
382
|
artist: Optional[str] = None
|
|
383
|
-
artist_ids: Optional[
|
|
383
|
+
artist_ids: Optional[list[UUID]] = None
|
|
384
384
|
booster: bool
|
|
385
385
|
border_color: ScryBorderColor
|
|
386
386
|
card_back_id: Optional[UUID] = None
|
|
387
387
|
collector_number: str
|
|
388
388
|
content_warning: Optional[bool] = None
|
|
389
389
|
digital: bool
|
|
390
|
-
finishes:
|
|
390
|
+
finishes: list[ScryFinish]
|
|
391
391
|
flavor_name: Optional[str] = None
|
|
392
392
|
flavor_text: Optional[str] = None
|
|
393
393
|
frame_effect: Optional[ScryFrameEffect] = None
|
|
394
|
-
frame_effects: Optional[
|
|
394
|
+
frame_effects: Optional[list[ScryFrameEffect]] = None
|
|
395
395
|
frame: ScryCardFrame
|
|
396
396
|
full_art: bool
|
|
397
|
-
games:
|
|
397
|
+
games: list[ScryGame]
|
|
398
398
|
highres_image: bool
|
|
399
399
|
illustration_id: Optional[UUID] = None
|
|
400
400
|
image_status: ScryImageStatus
|
|
401
|
-
image_uris: Optional[
|
|
402
|
-
prices: Optional[
|
|
401
|
+
image_uris: Optional[dict[str, str]] = None
|
|
402
|
+
prices: Optional[dict[str, Optional[Decimal]]] # TODO: enum keys=None
|
|
403
403
|
printed_name: Optional[str] = None
|
|
404
404
|
printed_text: Optional[str] = None
|
|
405
405
|
printed_type_line: Optional[str] = None
|
|
406
406
|
promo: bool
|
|
407
|
-
promo_types: Optional[
|
|
408
|
-
purchase_uris: Optional[
|
|
407
|
+
promo_types: Optional[list[str]] = None
|
|
408
|
+
purchase_uris: Optional[dict[str, str]] = None
|
|
409
409
|
rarity: ScryRarity
|
|
410
|
-
related_uris: Optional[
|
|
410
|
+
related_uris: Optional[dict[str, str]] = None
|
|
411
411
|
released_at: dt.date
|
|
412
412
|
reprint: bool
|
|
413
413
|
scryfall_set_uri: str
|
|
@@ -485,8 +485,8 @@ class ScryList(
|
|
|
485
485
|
):
|
|
486
486
|
"""Model for https://scryfall.com/docs/api/lists."""
|
|
487
487
|
|
|
488
|
-
data:
|
|
488
|
+
data: list[_ScryListableT]
|
|
489
489
|
has_more: bool
|
|
490
490
|
next_page: Optional[str] = None
|
|
491
491
|
total_cards: Optional[int] = None
|
|
492
|
-
warnings: Optional[
|
|
492
|
+
warnings: Optional[list[str]] = None
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import csv
|
|
4
4
|
import datetime as dt
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any, ClassVar,
|
|
7
|
+
from typing import Any, ClassVar, Optional
|
|
7
8
|
|
|
8
9
|
from mtg_ssm.containers import counts
|
|
9
10
|
from mtg_ssm.containers.collection import MagicCollection
|
|
@@ -15,7 +16,7 @@ from mtg_ssm.serialization import interface
|
|
|
15
16
|
CSV_HEADER = ["set", "name", "collector_number", "scryfall_id"] + [ct.value for ct in CountType]
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def row_for_card(card: ScryCard, card_count: Mapping[CountType, int]) ->
|
|
19
|
+
def row_for_card(card: ScryCard, card_count: Mapping[CountType, int]) -> dict[str, Any]:
|
|
19
20
|
"""Given a CardPrinting and counts, return a csv row."""
|
|
20
21
|
return {
|
|
21
22
|
"set": card.set.upper(),
|
|
@@ -26,7 +27,7 @@ def row_for_card(card: ScryCard, card_count: Mapping[CountType, int]) -> Dict[st
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def rows_for_cards(collection: MagicCollection, verbose: bool) -> Iterable[
|
|
30
|
+
def rows_for_cards(collection: MagicCollection, verbose: bool) -> Iterable[dict[str, Any]]:
|
|
30
31
|
"""Yield csv rows from a collection."""
|
|
31
32
|
for card_set in sorted(
|
|
32
33
|
collection.oracle.index.setcode_to_set.values(),
|
|
@@ -41,8 +42,8 @@ def rows_for_cards(collection: MagicCollection, verbose: bool) -> Iterable[Dict[
|
|
|
41
42
|
class CsvFullDialect(interface.SerializationDialect):
|
|
42
43
|
"""csv collection writing a row for every printing."""
|
|
43
44
|
|
|
44
|
-
extension: ClassVar[str] = "csv"
|
|
45
|
-
dialect: ClassVar[str] = "csv"
|
|
45
|
+
extension: ClassVar[Optional[str]] = "csv"
|
|
46
|
+
dialect: ClassVar[Optional[str]] = "csv"
|
|
46
47
|
|
|
47
48
|
verbose: ClassVar[bool] = True
|
|
48
49
|
|
|
@@ -65,6 +66,6 @@ class CsvFullDialect(interface.SerializationDialect):
|
|
|
65
66
|
class CsvTerseDialect(CsvFullDialect):
|
|
66
67
|
"""csv collection writing only rows that have counts."""
|
|
67
68
|
|
|
68
|
-
dialect: ClassVar[str] = "terse"
|
|
69
|
+
dialect: ClassVar[Optional[str]] = "terse"
|
|
69
70
|
|
|
70
71
|
verbose: ClassVar[bool] = False
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import ClassVar,
|
|
5
|
+
from typing import ClassVar, Optional
|
|
6
6
|
|
|
7
7
|
from mtg_ssm.containers.collection import MagicCollection
|
|
8
8
|
from mtg_ssm.containers.indexes import Oracle
|
|
@@ -23,13 +23,13 @@ class DeserializationError(Error):
|
|
|
23
23
|
class SerializationDialect(metaclass=abc.ABCMeta):
|
|
24
24
|
"""Abstract interface for mtg ssm serialization dialect."""
|
|
25
25
|
|
|
26
|
-
_EXT_DIALECT_DOC: ClassVar[
|
|
27
|
-
_EXT_DIALECT_TO_IMPL: ClassVar[
|
|
26
|
+
_EXT_DIALECT_DOC: ClassVar[set[tuple[str, str, str]]] = set()
|
|
27
|
+
_EXT_DIALECT_TO_IMPL: ClassVar[dict[tuple[str, str], type["SerializationDialect"]]] = {}
|
|
28
28
|
|
|
29
29
|
extension: ClassVar[Optional[str]] = None
|
|
30
30
|
dialect: ClassVar[Optional[str]] = None
|
|
31
31
|
|
|
32
|
-
def __init_subclass__(cls:
|
|
32
|
+
def __init_subclass__(cls: type["SerializationDialect"]) -> None:
|
|
33
33
|
super().__init_subclass__()
|
|
34
34
|
if cls.extension is not None and cls.dialect is not None:
|
|
35
35
|
cls._EXT_DIALECT_DOC.add((cls.extension, cls.dialect, cls.__doc__ or cls.__name__))
|
|
@@ -45,17 +45,17 @@ class SerializationDialect(metaclass=abc.ABCMeta):
|
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
47
|
def dialects(
|
|
48
|
-
cls:
|
|
49
|
-
) ->
|
|
48
|
+
cls: type["SerializationDialect"],
|
|
49
|
+
) -> list[tuple[str, Optional[str], Optional[str]]]:
|
|
50
50
|
"""List of (extension, dialect, description) of registered dialects."""
|
|
51
51
|
return sorted((ext, dial or "", doc or "") for ext, dial, doc in cls._EXT_DIALECT_DOC)
|
|
52
52
|
|
|
53
53
|
@classmethod
|
|
54
54
|
def by_extension(
|
|
55
|
-
cls:
|
|
55
|
+
cls: type["SerializationDialect"],
|
|
56
56
|
extension: str,
|
|
57
|
-
dialect_mappings:
|
|
58
|
-
) ->
|
|
57
|
+
dialect_mappings: dict[str, str],
|
|
58
|
+
) -> type["SerializationDialect"]:
|
|
59
59
|
"""Get a serializer class for a given extension and dialect mapping."""
|
|
60
60
|
dialect = dialect_mappings.get(extension, extension)
|
|
61
61
|
try:
|
|
@@ -5,8 +5,9 @@ import datetime as dt
|
|
|
5
5
|
import importlib.util
|
|
6
6
|
import itertools
|
|
7
7
|
import string
|
|
8
|
+
from collections.abc import Iterable, Sequence
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any, ClassVar,
|
|
10
|
+
from typing import Any, ClassVar, Optional
|
|
10
11
|
|
|
11
12
|
import openpyxl
|
|
12
13
|
from openpyxl.styles.numbers import FORMAT_CURRENCY_USD_SIMPLE
|
|
@@ -43,7 +44,7 @@ ALL_SETS_SHEET_TOTALS: Sequence[Optional[str]] = ["Total", None, None, None, Non
|
|
|
43
44
|
]
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def _card_set_sort_key(cset: ScrySet) ->
|
|
47
|
+
def _card_set_sort_key(cset: ScrySet) -> tuple[dt.date, str, int, str]:
|
|
47
48
|
released_at = cset.released_at or dt.date.min
|
|
48
49
|
return (
|
|
49
50
|
released_at,
|
|
@@ -123,7 +124,7 @@ def create_haverefs(index: ScryfallDataIndex, setcode: str, cards: Sequence[Scry
|
|
|
123
124
|
|
|
124
125
|
|
|
125
126
|
def get_references(
|
|
126
|
-
index: ScryfallDataIndex, card_name: str, exclude_sets: Optional[
|
|
127
|
+
index: ScryfallDataIndex, card_name: str, exclude_sets: Optional[set[str]] = None
|
|
127
128
|
) -> Optional[str]:
|
|
128
129
|
"""Get an equation for the references to a card."""
|
|
129
130
|
if util.is_strict_basic(card_name):
|
|
@@ -132,7 +133,7 @@ def get_references(
|
|
|
132
133
|
if exclude_sets is None:
|
|
133
134
|
exclude_sets = set()
|
|
134
135
|
|
|
135
|
-
set_to_cards:
|
|
136
|
+
set_to_cards: dict[str, list[ScryCard]] = collections.defaultdict(list)
|
|
136
137
|
for other_card in index.name_to_cards[card_name]:
|
|
137
138
|
if other_card.set not in exclude_sets:
|
|
138
139
|
set_to_cards[other_card.set].append(other_card)
|
|
@@ -238,7 +239,7 @@ def create_set_sheet(sheet: Worksheet, collection: MagicCollection, setcode: str
|
|
|
238
239
|
|
|
239
240
|
for card in index.setcode_to_cards[setcode]:
|
|
240
241
|
rownum = ROW_OFFSET + index.id_to_setindex[card.id]
|
|
241
|
-
row:
|
|
242
|
+
row: list[Optional[Any]] = [
|
|
242
243
|
HAVE_TMPL.format(rownum=rownum),
|
|
243
244
|
VALUE_TMPL.format(rownum=rownum),
|
|
244
245
|
card.name,
|
|
@@ -279,7 +280,7 @@ def style_set_sheet(sheet: Worksheet) -> None:
|
|
|
279
280
|
cdim.number_format = number_format
|
|
280
281
|
|
|
281
282
|
|
|
282
|
-
def rows_from_sheet(sheet: Worksheet) -> Iterable[
|
|
283
|
+
def rows_from_sheet(sheet: Worksheet) -> Iterable[dict[str, str]]:
|
|
283
284
|
"""Read rows from an xlsx worksheet as dicts."""
|
|
284
285
|
header_row, *rows = iter(sheet.rows)
|
|
285
286
|
header = [cell.value for cell in header_row]
|
|
@@ -290,8 +291,8 @@ def rows_from_sheet(sheet: Worksheet) -> Iterable[Dict[str, str]]:
|
|
|
290
291
|
|
|
291
292
|
|
|
292
293
|
def rows_for_workbook(
|
|
293
|
-
book: Workbook, *, skip_sheets: Optional[
|
|
294
|
-
) -> Iterable[
|
|
294
|
+
book: Workbook, *, skip_sheets: Optional[set[str]]
|
|
295
|
+
) -> Iterable[dict[str, str]]:
|
|
295
296
|
"""Read rows from an xlsx workbook as dicts."""
|
|
296
297
|
if skip_sheets is None:
|
|
297
298
|
skip_sheets = set()
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import argparse
|
|
5
5
|
import datetime as dt
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Optional
|
|
8
8
|
|
|
9
9
|
import mtg_ssm
|
|
10
10
|
import mtg_ssm.serialization.interface as ser_interface
|
|
@@ -24,7 +24,7 @@ def epilog() -> str:
|
|
|
24
24
|
return dialect_docs
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def set_type_list(value: str) ->
|
|
27
|
+
def set_type_list(value: str) -> set[ScrySetType]:
|
|
28
28
|
"""Argparse type to convert a string to a set of Scryfall Set Types."""
|
|
29
29
|
set_types = set()
|
|
30
30
|
for set_str in value.split(","):
|
|
@@ -39,7 +39,7 @@ def set_type_list(value: str) -> Set[ScrySetType]:
|
|
|
39
39
|
return set_types
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def card_layout_list(value: str) ->
|
|
42
|
+
def card_layout_list(value: str) -> set[ScryCardLayout]:
|
|
43
43
|
"""Argparse type to convert a string to a set of Scryfall Card Layouts."""
|
|
44
44
|
card_layouts = set()
|
|
45
45
|
for layout_str in value.split(","):
|
|
@@ -54,7 +54,7 @@ def card_layout_list(value: str) -> Set[ScryCardLayout]:
|
|
|
54
54
|
return card_layouts
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def get_args(args: Optional[
|
|
57
|
+
def get_args(args: Optional[list[str]] = None) -> argparse.Namespace:
|
|
58
58
|
"""Parse and return application arguments."""
|
|
59
59
|
parser = argparse.ArgumentParser(
|
|
60
60
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -179,8 +179,8 @@ def get_args(args: Optional[List[str]] = None) -> argparse.Namespace:
|
|
|
179
179
|
|
|
180
180
|
def get_oracle(
|
|
181
181
|
*,
|
|
182
|
-
exclude_set_types:
|
|
183
|
-
exclude_card_layouts:
|
|
182
|
+
exclude_set_types: set[ScrySetType],
|
|
183
|
+
exclude_card_layouts: set[ScryCardLayout],
|
|
184
184
|
include_digital: bool,
|
|
185
185
|
include_foreign_only: bool,
|
|
186
186
|
separate_promos: bool,
|
|
@@ -199,7 +199,7 @@ def get_oracle(
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def get_serializer(
|
|
202
|
-
dialect_mapping:
|
|
202
|
+
dialect_mapping: dict[str, str], path: Path
|
|
203
203
|
) -> ser_interface.SerializationDialect:
|
|
204
204
|
"""Retrieve a serializer compatible with a given filename."""
|
|
205
205
|
extension = path.suffix.lstrip(".")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: mtg_ssm
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: A tool to manage Magic: the Gathering collection spreadsheets
|
|
5
5
|
Author-email: George Leslie-Waksman <waksman@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -19,18 +19,18 @@ Classifier: Operating System :: Unix
|
|
|
19
19
|
Classifier: Programming Language :: Python
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
27
|
Classifier: Topic :: Games/Entertainment
|
|
28
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
29
|
Description-Content-Type: text/x-rst
|
|
30
30
|
License-File: LICENSE.txt
|
|
31
31
|
Requires-Dist: appdirs~=1.4
|
|
32
32
|
Requires-Dist: msgspec~=0.15
|
|
33
|
-
Requires-Dist: openpyxl~=3.
|
|
33
|
+
Requires-Dist: openpyxl~=3.1
|
|
34
34
|
Requires-Dist: requests~=2.27
|
|
35
35
|
Requires-Dist: requests-cache~=0.9.8
|
|
36
36
|
Requires-Dist: typing_extensions~=4.8.0
|
|
@@ -240,6 +240,12 @@ Development
|
|
|
240
240
|
|
|
241
241
|
- ...
|
|
242
242
|
|
|
243
|
+
2.7.0
|
|
244
|
+
-----
|
|
245
|
+
|
|
246
|
+
- Drop support for Python 3.8
|
|
247
|
+
- Add support for Python 3.13
|
|
248
|
+
|
|
243
249
|
2.6.9
|
|
244
250
|
-----
|
|
245
251
|
|
|
@@ -7,7 +7,7 @@ name = "mtg_ssm"
|
|
|
7
7
|
description = "A tool to manage Magic: the Gathering collection spreadsheets"
|
|
8
8
|
readme = "README.rst"
|
|
9
9
|
authors = [{ name = "George Leslie-Waksman", email = "waksman@gmail.com" }]
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
11
|
keywords = ["mtg", "magic", "collection", "tracking", "spreadsheet"]
|
|
12
12
|
license = { text = "MIT" }
|
|
13
13
|
classifiers = [
|
|
@@ -23,18 +23,18 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python",
|
|
24
24
|
"Programming Language :: Python :: 3",
|
|
25
25
|
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
-
"Programming Language :: Python :: 3.8",
|
|
27
26
|
"Programming Language :: Python :: 3.9",
|
|
28
27
|
"Programming Language :: Python :: 3.10",
|
|
29
28
|
"Programming Language :: Python :: 3.11",
|
|
30
29
|
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
31
|
"Topic :: Games/Entertainment",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
dependencies = [
|
|
35
35
|
"appdirs~=1.4",
|
|
36
36
|
"msgspec~=0.15",
|
|
37
|
-
"openpyxl~=3.
|
|
37
|
+
"openpyxl~=3.1",
|
|
38
38
|
"requests~=2.27",
|
|
39
39
|
"requests-cache~=0.9.8",
|
|
40
40
|
"typing_extensions~=4.8.0",
|
|
@@ -93,7 +93,7 @@ addopts = "--cov mtg_ssm --cov-report term-missing"
|
|
|
93
93
|
testpaths = ["tests"]
|
|
94
94
|
|
|
95
95
|
[tool.mypy]
|
|
96
|
-
python_version = "3.
|
|
96
|
+
python_version = "3.9"
|
|
97
97
|
|
|
98
98
|
follow_imports = "normal"
|
|
99
99
|
disallow_untyped_defs = true
|
|
@@ -101,10 +101,10 @@ allow_untyped_globals = false
|
|
|
101
101
|
|
|
102
102
|
[tool.black]
|
|
103
103
|
line-length = 99
|
|
104
|
-
target-version = ["
|
|
104
|
+
target-version = ["py39"]
|
|
105
105
|
|
|
106
106
|
[tool.ruff]
|
|
107
|
-
target-version = "
|
|
107
|
+
target-version = "py39"
|
|
108
108
|
line-length = 99
|
|
109
109
|
|
|
110
110
|
[tool.ruff.lint]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""pytest test configuration file."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Generator
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, Generator, List
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
8
|
import msgspec
|
|
@@ -46,14 +46,14 @@ def requests_mock() -> Generator[responses.RequestsMock, None, None]:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@pytest.fixture(scope="session")
|
|
49
|
-
def cards_data() ->
|
|
49
|
+
def cards_data() -> list[ScryCard]:
|
|
50
50
|
"""Fixture containing all test card data."""
|
|
51
51
|
with CARDS_DATA_FILE.open("rb") as card_data_file:
|
|
52
|
-
return msgspec.json.decode(card_data_file.read(), type=
|
|
52
|
+
return msgspec.json.decode(card_data_file.read(), type=list[ScryCard])
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
@pytest.fixture(scope="session")
|
|
56
|
-
def sets_data() ->
|
|
56
|
+
def sets_data() -> list[ScrySet]:
|
|
57
57
|
"""Fixture containing all test set data."""
|
|
58
58
|
with SETS_DATA_FILE.open("rb") as sets_data_file:
|
|
59
59
|
sets_list = msgspec.json.decode(sets_data_file.read(), type=ScryList[ScrySet])
|
|
@@ -61,7 +61,7 @@ def sets_data() -> List[ScrySet]:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
@pytest.fixture(scope="session")
|
|
64
|
-
def migrations_data() ->
|
|
64
|
+
def migrations_data() -> list[ScryMigration]:
|
|
65
65
|
"""Fixture containing all test migrations data."""
|
|
66
66
|
with MIGRATIONS_DATA_FILE.open("rb") as migrations_data_file:
|
|
67
67
|
migrations_list = msgspec.json.decode(
|
|
@@ -71,16 +71,16 @@ def migrations_data() -> List[ScryMigration]:
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
@pytest.fixture(scope="session")
|
|
74
|
-
def id_to_card(cards_data:
|
|
74
|
+
def id_to_card(cards_data: list[ScryCard]) -> dict[UUID, ScryCard]:
|
|
75
75
|
"""Fixture returning scryfall id to card object for all test card data."""
|
|
76
76
|
return {card.id: card for card in cards_data}
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
@pytest.fixture(scope="session")
|
|
80
80
|
def scryfall_data(
|
|
81
|
-
cards_data:
|
|
82
|
-
sets_data:
|
|
83
|
-
migrations_data:
|
|
81
|
+
cards_data: list[ScryCard],
|
|
82
|
+
sets_data: list[ScrySet],
|
|
83
|
+
migrations_data: list[ScryMigration],
|
|
84
84
|
) -> ScryfallDataSet:
|
|
85
85
|
"""Fixture containing all scryfall test data."""
|
|
86
86
|
return ScryfallDataSet(sets=sets_data, cards=cards_data, migrations=migrations_data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tests for mtg_ssm.mtg.counts."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
@@ -50,7 +50,7 @@ def oracle(scryfall_data: ScryfallDataSet) -> Oracle:
|
|
|
50
50
|
],
|
|
51
51
|
)
|
|
52
52
|
def test_merge_card_counts(
|
|
53
|
-
in_card_counts:
|
|
53
|
+
in_card_counts: list[ScryfallCardCount], out_card_count: ScryfallCardCount
|
|
54
54
|
) -> None:
|
|
55
55
|
assert counts.merge_card_counts(*in_card_counts) == out_card_count
|
|
56
56
|
|
|
@@ -193,6 +193,6 @@ def test_diff_card_counts(
|
|
|
193
193
|
],
|
|
194
194
|
)
|
|
195
195
|
def test_aggregate_card_counts(
|
|
196
|
-
oracle: Oracle, card_rows:
|
|
196
|
+
oracle: Oracle, card_rows: list[dict[str, Any]], output: counts.ScryfallCardCount
|
|
197
197
|
) -> None:
|
|
198
198
|
assert counts.aggregate_card_counts(card_rows, oracle) == output
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tests for mtg_ssm.containers.legacy."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
@@ -30,7 +30,7 @@ def oracle(scryfall_data: ScryfallDataSet) -> Oracle:
|
|
|
30
30
|
),
|
|
31
31
|
],
|
|
32
32
|
)
|
|
33
|
-
def test_extract_counts(card_row:
|
|
33
|
+
def test_extract_counts(card_row: dict[str, Any], expected: dict[str, int]) -> None:
|
|
34
34
|
assert legacy.extract_counts(card_row) == expected
|
|
35
35
|
|
|
36
36
|
|
|
@@ -86,7 +86,7 @@ def test_extract_counts(card_row: Dict[str, Any], expected: Dict[str, int]) -> N
|
|
|
86
86
|
),
|
|
87
87
|
],
|
|
88
88
|
)
|
|
89
|
-
def test_find_scryfall_id(card_row:
|
|
89
|
+
def test_find_scryfall_id(card_row: dict[str, Any], expected: UUID, oracle: Oracle) -> None:
|
|
90
90
|
assert legacy.find_scryfall_id(card_row, oracle) == expected
|
|
91
91
|
|
|
92
92
|
|
|
@@ -101,5 +101,5 @@ def test_find_scryfall_id(card_row: Dict[str, Any], expected: UUID, oracle: Orac
|
|
|
101
101
|
),
|
|
102
102
|
],
|
|
103
103
|
)
|
|
104
|
-
def test_coerce_row(card_row:
|
|
104
|
+
def test_coerce_row(card_row: dict[str, Any], expected: dict[str, Any], oracle: Oracle) -> None:
|
|
105
105
|
assert legacy.coerce_row(card_row, oracle) == expected
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Tests for mtg_ssm.mtg.util."""
|
|
2
2
|
|
|
3
|
-
from typing import Dict, Tuple
|
|
4
3
|
from uuid import UUID
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
@@ -44,10 +43,10 @@ def test_is_strict_basic(name: str, expected: bool) -> None:
|
|
|
44
43
|
],
|
|
45
44
|
)
|
|
46
45
|
def test_collector_number_parts(
|
|
47
|
-
id_to_card:
|
|
46
|
+
id_to_card: dict[UUID, ScryCard],
|
|
48
47
|
name: str,
|
|
49
48
|
card_id: UUID,
|
|
50
|
-
number_parts:
|
|
49
|
+
number_parts: tuple[str, int, str],
|
|
51
50
|
) -> None:
|
|
52
51
|
card = id_to_card[card_id]
|
|
53
52
|
assert card.name == name
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from
|
|
5
|
+
from re import Pattern
|
|
6
|
+
from typing import Union
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
from responses import RequestsMock
|
|
@@ -14,7 +15,7 @@ from tests import gen_testdata
|
|
|
14
15
|
|
|
15
16
|
BULK_CARDS_REGEX = r"https://data\.scryfall\.io/default-cards/default-cards-\d{14}\.json"
|
|
16
17
|
|
|
17
|
-
ENDPOINT_TO_FILE:
|
|
18
|
+
ENDPOINT_TO_FILE: dict[Union[str, Pattern[str]], Path] = {
|
|
18
19
|
fetcher.BULK_DATA_ENDPOINT: gen_testdata.TARGET_BULK_FILE,
|
|
19
20
|
fetcher.SETS_ENDPOINT: gen_testdata.TARGET_SETS_FILE1,
|
|
20
21
|
gen_testdata.SETS_NEXTPAGE_URL: gen_testdata.TARGET_SETS_FILE2,
|
|
@@ -52,9 +53,9 @@ def test_scryfetch() -> None:
|
|
|
52
53
|
@pytest.mark.usefixtures("_scryurls")
|
|
53
54
|
def test_data_fixtures(
|
|
54
55
|
scryfall_data: ScryfallDataSet,
|
|
55
|
-
sets_data:
|
|
56
|
-
cards_data:
|
|
57
|
-
migrations_data:
|
|
56
|
+
sets_data: list[ScrySet],
|
|
57
|
+
cards_data: list[ScryCard],
|
|
58
|
+
migrations_data: list[ScryMigration],
|
|
58
59
|
) -> None:
|
|
59
60
|
scrydata = fetcher.scryfetch()
|
|
60
61
|
assert scrydata == scryfall_data
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import textwrap
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Dict
|
|
6
5
|
from uuid import UUID
|
|
7
6
|
|
|
8
7
|
import pytest
|
|
@@ -42,7 +41,7 @@ def test_header() -> None:
|
|
|
42
41
|
]
|
|
43
42
|
|
|
44
43
|
|
|
45
|
-
def test_row_for_card(id_to_card:
|
|
44
|
+
def test_row_for_card(id_to_card: dict[UUID, ScryCard]) -> None:
|
|
46
45
|
card = id_to_card[TEST_CARD_ID]
|
|
47
46
|
card_counts = {counts.CountType.NONFOIL: 3, counts.CountType.FOIL: 5}
|
|
48
47
|
csv_row = csv_serializer.row_for_card(card, card_counts)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Tests for mtg_ssm.serialization.interface.py."""
|
|
2
2
|
|
|
3
|
-
from typing import Dict
|
|
4
3
|
from unittest import mock
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
@@ -40,7 +39,7 @@ def test_all_dialects() -> None:
|
|
|
40
39
|
],
|
|
41
40
|
)
|
|
42
41
|
def test_extension_lookup(
|
|
43
|
-
extension: str, dialect_mapping:
|
|
42
|
+
extension: str, dialect_mapping: dict[str, str], dialect_name: str
|
|
44
43
|
) -> None:
|
|
45
44
|
serialization_class = interface.SerializationDialect.by_extension(extension, dialect_mapping)
|
|
46
45
|
assert isinstance(serialization_class, type)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Tests for mtg_ssm.serialization.xlsx."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Optional
|
|
5
5
|
from uuid import UUID
|
|
6
6
|
|
|
7
7
|
import openpyxl
|
|
@@ -74,7 +74,7 @@ def test_create_haverefs(oracle: Oracle) -> None:
|
|
|
74
74
|
],
|
|
75
75
|
)
|
|
76
76
|
def test_get_references(
|
|
77
|
-
oracle: Oracle, name: str, exclude_sets: Optional[
|
|
77
|
+
oracle: Oracle, name: str, exclude_sets: Optional[set[str]], expected: str
|
|
78
78
|
) -> None:
|
|
79
79
|
print_refs = xlsx_serializer.get_references(oracle.index, name, exclude_sets=exclude_sets)
|
|
80
80
|
assert print_refs == expected
|
|
@@ -160,9 +160,9 @@ def test_write(snapshot: SnapshotAssertion, oracle: Oracle, tmp_path: Path) -> N
|
|
|
160
160
|
],
|
|
161
161
|
)
|
|
162
162
|
def test_rows_from_workbook(
|
|
163
|
-
sheets_and_rows:
|
|
164
|
-
skip_sheets: Optional[
|
|
165
|
-
expected:
|
|
163
|
+
sheets_and_rows: list[tuple[str, list[list[str]]]],
|
|
164
|
+
skip_sheets: Optional[set[str]],
|
|
165
|
+
expected: list[dict[str, str]],
|
|
166
166
|
) -> None:
|
|
167
167
|
workbook = openpyxl.Workbook()
|
|
168
168
|
for sheet, rows in sheets_and_rows:
|
|
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
|
|
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
|
|
File without changes
|
{mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__snapshots__/test_csv_serializer.ambr
RENAMED
|
File without changes
|
{mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__snapshots__/test_xlsx_serializer.ambr
RENAMED
|
File without changes
|
|
File without changes
|