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.
Files changed (65) hide show
  1. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/integration.yml +1 -1
  2. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/pre-commit.yml +1 -1
  3. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/publish.yml +1 -1
  4. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/workflows/run-tests.yml +2 -2
  5. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/PKG-INFO +11 -5
  6. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/README.rst +6 -0
  7. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/_version.py +2 -2
  8. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/bundles.py +7 -13
  9. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/counts.py +4 -3
  10. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/indexes.py +20 -19
  11. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/legacy.py +7 -7
  12. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/mtg/util.py +2 -3
  13. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/scryfall/fetcher.py +2 -9
  14. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/scryfall/models.py +24 -24
  15. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/csv_serializer.py +7 -6
  16. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/interface.py +9 -9
  17. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/xlsx_serializer.py +9 -8
  18. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/ssm.py +7 -7
  19. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/PKG-INFO +11 -5
  20. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/requires.txt +1 -1
  21. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/pyproject.toml +6 -6
  22. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/conftest.py +9 -9
  23. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_counts.py +3 -3
  24. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_legacy.py +4 -4
  25. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/mtg/test_util.py +2 -3
  26. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/scryfall/test_fetcher.py +6 -5
  27. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/test_csv_serializer.py +1 -2
  28. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/test_interface.py +1 -2
  29. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/test_xlsx_serializer.py +5 -5
  30. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.editorconfig +0 -0
  31. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.github/dependabot.yml +0 -0
  32. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.gitignore +0 -0
  33. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/.pre-commit-config.yaml +0 -0
  34. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/LICENSE.txt +0 -0
  35. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/TODO.md +0 -0
  36. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/__init__.py +0 -0
  37. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/__init__.py +0 -0
  38. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/containers/collection.py +0 -0
  39. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/mtg/__init__.py +0 -0
  40. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/py.typed +0 -0
  41. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/scryfall/__init__.py +0 -0
  42. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm/serialization/__init__.py +0 -0
  43. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/SOURCES.txt +0 -0
  44. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/dependency_links.txt +0 -0
  45. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/entry_points.txt +0 -0
  46. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/mtg_ssm.egg-info/top_level.txt +0 -0
  47. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/setup.cfg +0 -0
  48. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/__init__.py +0 -0
  49. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/__init__.py +0 -0
  50. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_bundles.py +0 -0
  51. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/containers/test_indexes.py +0 -0
  52. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/LICENSE.txt +0 -0
  53. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/bulk_data.json +0 -0
  54. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/cards.json +0 -0
  55. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/migrations.json +0 -0
  56. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/sets.json +0 -0
  57. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/sets1.json +0 -0
  58. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/data/sets2.json +0 -0
  59. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/gen_testdata.py +0 -0
  60. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/mtg/__init__.py +0 -0
  61. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/scryfall/__init__.py +0 -0
  62. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__init__.py +0 -0
  63. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__snapshots__/test_csv_serializer.ambr +0 -0
  64. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/serialization/__snapshots__/test_xlsx_serializer.ambr +0 -0
  65. {mtg_ssm-2.6.12.dev4 → mtg_ssm-2.7.0}/tests/test_ssm.py +0 -0
@@ -24,7 +24,7 @@ jobs:
24
24
 
25
25
  - uses: actions/setup-python@v5
26
26
  with:
27
- python-version: "3.8"
27
+ python-version: "3.13"
28
28
  cache: pip
29
29
  cache-dependency-path: |
30
30
  pyproject.toml
@@ -15,7 +15,7 @@ jobs:
15
15
 
16
16
  - uses: actions/setup-python@v5
17
17
  with:
18
- python-version: "3.12"
18
+ python-version: "3.9"
19
19
  cache: pip
20
20
  cache-dependency-path: pyproject.toml
21
21
 
@@ -23,7 +23,7 @@ jobs:
23
23
 
24
24
  - uses: actions/setup-python@v5
25
25
  with:
26
- python-version: "3.8"
26
+ python-version: "3.9"
27
27
 
28
28
  - name: Install pypa/build
29
29
  run: >-
@@ -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
1
+ Metadata-Version: 2.2
2
2
  Name: mtg_ssm
3
- Version: 2.6.12.dev4
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.8
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.0
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
 
@@ -179,6 +179,12 @@ Development
179
179
 
180
180
  - ...
181
181
 
182
+ 2.7.0
183
+ -----
184
+
185
+ - Drop support for Python 3.8
186
+ - Add support for Python 3.13
187
+
182
188
  2.6.9
183
189
  -----
184
190
 
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.6.12.dev4'
21
- __version_tuple__ = version_tuple = (2, 6, 12, 'dev4')
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 List, NamedTuple, Optional, Set
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: List[ScrySet]
19
- cards: List[ScryCard]
20
- migrations: List[ScryMigration]
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[Set[ScrySetType]] = None,
27
- exclude_card_layouts: Optional[Set[ScryCardLayout]] = None,
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 typing import Any, Dict, Iterable, MutableMapping
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 = Dict[UUID, MutableMapping[CountType, int]]
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[Dict[str, Any]], oracle: Oracle
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 typing import Dict, Iterable, List, Optional, Set, Tuple
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) -> Tuple[str, str, int, str]:
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) -> Tuple[str, int, str]:
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[Tuple[Optional[str], str, Optional[str], Optional[int], Optional[str]]]:
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: Set[Tuple[str, Optional[str]]] = {(card.name, card.collector_number)}
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: Set[Optional[str]] = {card.set, None}
36
+ sets: set[Optional[str]] = {card.set, None}
36
37
 
37
- mvids: Set[Optional[int]] = {None} | set(card.multiverse_ids or ())
38
+ mvids: set[Optional[int]] = {None} | set(card.multiverse_ids or ())
38
39
 
39
- artists: Set[Optional[str]] = {card.artist, None}
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: Dict[UUID, ScryCard] = {}
53
- self.name_to_cards: Dict[str, List[ScryCard]] = {}
54
- self.setcode_to_cards: Dict[str, List[ScryCard]] = {}
55
- self.id_to_setindex: Dict[UUID, int] = {}
56
- self.setcode_to_set: Dict[str, ScrySet] = {}
57
- self.migrate_old_id_to_new_id: Dict[UUID, UUID] = {}
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: Dict[
62
- Tuple[Optional[str], str, Optional[str], Optional[int], Optional[str]],
63
- Set[UUID],
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: Dict[str, List[ScryCard]] = collections.defaultdict(list)
75
- setcode_to_unsorted_cards: Dict[str, List[ScryCard]] = {}
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, Dict, List, Optional, Set, Tuple
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: Dict[str, Set[str]] = {
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: Dict[str, Any]) -> Dict[str, int]:
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: Dict[Optional[str], str] = {
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: Dict[str, str], oracle: Oracle) -> UUID:
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: List[Tuple[Optional[str], str, Optional[str], Optional[int], Optional[str]]] = []
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: Dict[str, Any], oracle: Oracle) -> Dict[str, Any]:
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.lru_cache(maxsize=None)
13
- def collector_number_parts(collector_number: str) -> Tuple[str, int, 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=List[ScryCard])
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 Dict, Generic, List, Optional, TypeVar, Union
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[List[ScryColor]] = None
303
- colors: Optional[List[ScryColor]] = None
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[Dict[str, str]] = None
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[List[int]] = None
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[List[ScryRelatedCard]] = None
357
- card_faces: Optional[List[ScryCardFace]] = None
356
+ all_parts: Optional[list[ScryRelatedCard]] = None
357
+ card_faces: Optional[list[ScryCardFace]] = None
358
358
  cmc: Optional[float] = None
359
- colors: Optional[List[ScryColor]] = None
360
- color_identity: List[ScryColor]
361
- color_indicator: Optional[List[ScryColor]] = None
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: List[str]
365
+ keywords: list[str]
366
366
  layout: ScryCardLayout
367
- legalities: Dict[ScryFormat, ScryLegality]
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[List[str]] = None
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[List[UUID]] = None
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: List[ScryFinish]
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[List[ScryFrameEffect]] = None
394
+ frame_effects: Optional[list[ScryFrameEffect]] = None
395
395
  frame: ScryCardFrame
396
396
  full_art: bool
397
- games: List[ScryGame]
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[Dict[str, str]] = None
402
- prices: Optional[Dict[str, Optional[Decimal]]] # TODO: enum keys=None
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[List[str]] = None
408
- purchase_uris: Optional[Dict[str, str]] = None
407
+ promo_types: Optional[list[str]] = None
408
+ purchase_uris: Optional[dict[str, str]] = None
409
409
  rarity: ScryRarity
410
- related_uris: Optional[Dict[str, str]] = None
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: List[_ScryListableT]
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[List[str]] = None
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, Dict, Iterable, Mapping
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]) -> Dict[str, Any]:
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[Dict[str, Any]]:
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, Dict, List, Optional, Set, Tuple, Type
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[Set[Tuple[str, str, str]]] = set()
27
- _EXT_DIALECT_TO_IMPL: ClassVar[Dict[Tuple[str, str], Type["SerializationDialect"]]] = {}
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: Type["SerializationDialect"]) -> None:
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: Type["SerializationDialect"],
49
- ) -> List[Tuple[str, Optional[str], Optional[str]]]:
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: Type["SerializationDialect"],
55
+ cls: type["SerializationDialect"],
56
56
  extension: str,
57
- dialect_mappings: Dict[str, str],
58
- ) -> Type["SerializationDialect"]:
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, Dict, Iterable, List, Optional, Sequence, Set, Tuple
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) -> Tuple[dt.date, str, int, str]:
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[Set[str]] = None
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: Dict[str, List[ScryCard]] = collections.defaultdict(list)
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: List[Optional[Any]] = [
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[Dict[str, str]]:
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[Set[str]]
294
- ) -> Iterable[Dict[str, str]]:
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 Dict, List, Optional, Set
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) -> Set[ScrySetType]:
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) -> Set[ScryCardLayout]:
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[List[str]] = None) -> argparse.Namespace:
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: Set[ScrySetType],
183
- exclude_card_layouts: Set[ScryCardLayout],
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: Dict[str, str], path: Path
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
1
+ Metadata-Version: 2.2
2
2
  Name: mtg_ssm
3
- Version: 2.6.12.dev4
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.8
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.0
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
 
@@ -1,6 +1,6 @@
1
1
  appdirs~=1.4
2
2
  msgspec~=0.15
3
- openpyxl~=3.0
3
+ openpyxl~=3.1
4
4
  requests~=2.27
5
5
  requests-cache~=0.9.8
6
6
  typing_extensions~=4.8.0
@@ -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.8"
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.0",
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.8"
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 = ["py38"]
104
+ target-version = ["py39"]
105
105
 
106
106
  [tool.ruff]
107
- target-version = "py38"
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() -> List[ScryCard]:
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=List[ScryCard])
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() -> List[ScrySet]:
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() -> List[ScryMigration]:
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: List[ScryCard]) -> Dict[UUID, ScryCard]:
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: List[ScryCard],
82
- sets_data: List[ScrySet],
83
- migrations_data: List[ScryMigration],
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, Dict, List
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: List[ScryfallCardCount], out_card_count: ScryfallCardCount
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: List[Dict[str, Any]], output: counts.ScryfallCardCount
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, Dict
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: Dict[str, Any], expected: Dict[str, int]) -> None:
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: Dict[str, Any], expected: UUID, oracle: Oracle) -> None:
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: Dict[str, Any], expected: Dict[str, Any], oracle: Oracle) -> None:
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: Dict[UUID, ScryCard],
46
+ id_to_card: dict[UUID, ScryCard],
48
47
  name: str,
49
48
  card_id: UUID,
50
- number_parts: Tuple[str, int, str],
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 typing import Dict, List, Pattern, Union
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: Dict[Union[str, Pattern[str]], Path] = {
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: List[ScrySet],
56
- cards_data: List[ScryCard],
57
- migrations_data: List[ScryMigration],
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: Dict[UUID, ScryCard]) -> None:
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: Dict[str, str], dialect_name: str
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 Dict, List, Optional, Set, Tuple
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[Set[str]], expected: str
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: List[Tuple[str, List[List[str]]]],
164
- skip_sheets: Optional[Set[str]],
165
- expected: List[Dict[str, str]],
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