aenir 0.0.post1.dev1365__tar.gz → 0.0.post1.dev1376__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 (64) hide show
  1. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/PKG-INFO +1 -1
  2. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/requirements-dev.txt +14 -14
  3. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/__init__.py +4 -1
  4. aenir-0.0.post1.dev1376/src/aenir/_archive/__init__.py +21 -0
  5. aenir-0.0.post1.dev1376/src/aenir/_archive/_base.py +52 -0
  6. aenir-0.0.post1.dev1376/src/aenir/_archive/_basemorph.py +345 -0
  7. aenir-0.0.post1.dev1376/src/aenir/_archive/cleaner.py +204 -0
  8. aenir-0.0.post1.dev1376/src/aenir/_archive/morph.py +729 -0
  9. aenir-0.0.post1.dev1376/src/aenir/_archive/scraper.py +63 -0
  10. aenir-0.0.post1.dev1376/src/aenir/_archive/shortcuts.py +26 -0
  11. aenir-0.0.post1.dev1376/src/aenir/_archive/transcriber.py +119 -0
  12. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/morph.py +175 -123
  13. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir.egg-info/PKG-INFO +1 -1
  14. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir.egg-info/SOURCES.txt +10 -0
  15. aenir-0.0.post1.dev1376/src/aenir.egg-info/scm_file_list.json +59 -0
  16. aenir-0.0.post1.dev1376/src/aenir.egg-info/scm_version.json +8 -0
  17. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tests/test_morph.py +164 -58
  18. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/.coveragerc +0 -0
  19. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/.gitignore +0 -0
  20. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/.readthedocs.yml +0 -0
  21. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/AUTHORS.md +0 -0
  22. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/CHANGELOG.md +0 -0
  23. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/CONTRIBUTING.md +0 -0
  24. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/LICENSE.txt +0 -0
  25. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/MANIFEST.in +0 -0
  26. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/README.md +0 -0
  27. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/TODO.md +0 -0
  28. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/Makefile +0 -0
  29. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/_static/.gitignore +0 -0
  30. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/authors.md +0 -0
  31. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/changelog.md +0 -0
  32. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/conf.py +0 -0
  33. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/contributing.md +0 -0
  34. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/index.md +0 -0
  35. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/license.md +0 -0
  36. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/readme.md +0 -0
  37. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/docs/requirements.txt +0 -0
  38. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/logo.png +0 -0
  39. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/pyproject.toml +0 -0
  40. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/requirements.txt +0 -0
  41. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/setup.cfg +0 -0
  42. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/setup.py +0 -0
  43. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/.create_table_from_csv.py +0 -0
  44. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/_exceptions.py +0 -0
  45. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/_logging.py +0 -0
  46. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/games.py +0 -0
  47. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/skeleton.py +0 -0
  48. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/static/binding-blade/cleaned_stats.db +0 -0
  49. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/static/blazing-sword/cleaned_stats.db +0 -0
  50. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/static/genealogy-of-the-holy-war/cleaned_stats.db +0 -0
  51. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/static/path-of-radiance/cleaned_stats.db +0 -0
  52. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/static/the-sacred-stones/cleaned_stats.db +0 -0
  53. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/static/thracia-776/cleaned_stats.db +0 -0
  54. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir/stats.py +0 -0
  55. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir.egg-info/dependency_links.txt +0 -0
  56. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir.egg-info/not-zip-safe +0 -0
  57. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir.egg-info/requires.txt +0 -0
  58. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/src/aenir.egg-info/top_level.txt +0 -0
  59. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tests/conftest.py +0 -0
  60. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tests/test_games.py +0 -0
  61. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tests/test_skeleton.py +0 -0
  62. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tests/test_stats.py +0 -0
  63. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tests/weapon.py +0 -0
  64. {aenir-0.0.post1.dev1365 → aenir-0.0.post1.dev1376}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aenir
3
- Version: 0.0.post1.dev1365
3
+ Version: 0.0.post1.dev1376
4
4
  Summary: Stat calculator for Fire Emblem
5
5
  Home-page: https://github.com/gchang12/aenir/
6
6
  Author: gchang12
@@ -1,17 +1,17 @@
1
1
  ## The following requirements were added by pip freeze:
2
2
  -e git+https://github.com/gchang12/aenir.git@a77487884c98365ea6dfad8be104b2087fe49755#egg=aenir
3
- cachetools==7.1.3
4
- certifi==2026.4.22
3
+ cachetools==7.1.4
4
+ certifi==2026.6.17
5
5
  cffi==2.0.0
6
6
  chardet==7.4.3
7
7
  charset-normalizer==3.4.7
8
8
  colorama==0.4.6
9
- cryptography==48.0.0
10
- distlib==0.4.0
11
- docutils==0.22.4
12
- filelock==3.29.0
9
+ cryptography
10
+ distlib==0.4.3
11
+ docutils==0.23
12
+ filelock==3.29.4
13
13
  id==1.6.1
14
- idna==3.15
14
+ idna==3.18
15
15
  jaraco.classes==3.4.0
16
16
  jaraco.context==6.1.2
17
17
  jaraco.functools==4.5.0
@@ -19,23 +19,23 @@ jeepney==0.9.0
19
19
  keyring==25.7.0
20
20
  markdown-it-py==4.2.0
21
21
  mdurl==0.1.2
22
- more-itertools==11.0.2
22
+ more-itertools==11.1.0
23
23
  nh3==0.3.5
24
24
  packaging==26.2
25
- platformdirs==4.9.6
25
+ platformdirs==4.10.0
26
26
  pluggy==1.6.0
27
27
  pycparser==3.0
28
28
  Pygments==2.20.0
29
- pyproject-api==1.10.0
30
- python-discovery==1.3.1
31
- readme_renderer==44.0
29
+ pyproject-api==1.10.1
30
+ python-discovery==1.4.2
31
+ readme_renderer==45.0
32
32
  requests==2.34.2
33
33
  requests-toolbelt==1.0.0
34
34
  rfc3986==2.0.0
35
35
  rich==15.0.0
36
36
  SecretStorage==3.5.0
37
37
  tomli_w==1.2.0
38
- tox==4.54.0
38
+ tox==4.55.1
39
39
  twine==6.2.0
40
40
  urllib3==2.7.0
41
- virtualenv==21.3.3
41
+ virtualenv==21.5.1
@@ -19,7 +19,10 @@ except PackageNotFoundError: # pragma: no cover
19
19
  finally:
20
20
  del version, PackageNotFoundError
21
21
 
22
- from aenir.morph import get_morph
22
+ from aenir.morph import (
23
+ get_morph,
24
+ get_morph_class,
25
+ )
23
26
  from aenir._exceptions import (
24
27
  AenirError,
25
28
  UnitNotFoundError,
@@ -0,0 +1,21 @@
1
+ import sys
2
+
3
+ from aenir.morph import (
4
+ Morph4, Morph5, Morph6, Morph7, Morph8, Morph9
5
+ )
6
+ from aenir.shortcuts import get_morph
7
+
8
+ if sys.version_info[:2] >= (3, 8):
9
+ # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
10
+ from importlib.metadata import PackageNotFoundError, version # pragma: no cover
11
+ else:
12
+ from importlib_metadata import PackageNotFoundError, version # pragma: no cover
13
+
14
+ try:
15
+ # Change here if project is renamed and does not equal the package name
16
+ dist_name = __name__
17
+ __version__ = version(dist_name)
18
+ except PackageNotFoundError: # pragma: no cover
19
+ __version__ = "unknown"
20
+ finally:
21
+ del version, PackageNotFoundError
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ Defines the SerenesBase class.
4
+
5
+ SerenesBase: Defines parameters for web-scraping.
6
+ """
7
+
8
+ class SerenesBase:
9
+ """
10
+ Defines parameters for web-scraping serenesforest.net.
11
+
12
+ URL_ROOT: URL of the website to be scraped.
13
+ NUM_TO_NAME: Index that looks up FE title by game number.
14
+ """
15
+
16
+ _NUM_TO_NAME = {
17
+ 4: "genealogy-of-the-holy-war",
18
+ 5: "thracia-776",
19
+ 6: "binding-blade",
20
+ 7: "blazing-sword",
21
+ 8: "the-sacred-stones",
22
+ 9: "path-of-radiance",
23
+ }
24
+
25
+ def __init__(self, game_num: int):
26
+ """
27
+ Defines parameters that identify a FE game.
28
+ """
29
+ self._game_num = game_num
30
+ #!will raise KeyError if not found
31
+ self._game_name = self.NUM_TO_NAME[self._game_num]
32
+
33
+ @property
34
+ def game_num(self):
35
+ """
36
+ The number of a FE game in the series.
37
+ """
38
+ return self._game_num
39
+
40
+ @property
41
+ def game_name(self):
42
+ """
43
+ The name of a FE game, given the number.
44
+ """
45
+ return self._game_name
46
+
47
+ @property
48
+ def NUM_TO_NAME(self):
49
+ """
50
+ Index that looks up FE title by game number.
51
+ """
52
+ return self._NUM_TO_NAME
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ Defines the BaseMorph class.
4
+
5
+ BaseMorph: Defines methods for verifying manual input, and matching it to the proper sources.
6
+ """
7
+
8
+ # TODO: Create docstrings. Organize documentation better.
9
+
10
+ import logging
11
+ import io
12
+ from typing import Tuple, List
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import pandas as pd
17
+ #from aenir.cleaner import SerenesCleaner
18
+
19
+ class ProtoMorph:
20
+ """
21
+ Defines parameters and methods for stat look-up and name verification.
22
+
23
+ current_clstype: Determines which table to match from.
24
+ current_cls: Value of the current class, to be matched.
25
+ target_stats: Stores the stats to be retrieved.
26
+ history: Stores the history of a unit as a list of (Class, Lv) tuples.
27
+ STAT_ORDERING: Stores the order of stat labels.
28
+ """
29
+
30
+ DATADIR_ROOT = "data"
31
+ STAT_ORDERING = {
32
+ 4: ["HP", "Str", "Mag", "Skl", "Spd", "Lck", "Def", "Res"],
33
+ 5: ["HP", "Str", "Mag", "Skl", "Spd", "Lck", "Def", "Con", "Mov"],
34
+ 6: ["HP", "Pow", "Skl", "Spd", "Lck", "Def", "Res"],
35
+ 7: ["HP", "Pow", "Skl", "Spd", "Lck", "Def", "Res"],
36
+ 8: ["HP", "Pow", "Skl", "Spd", "Lck", "Def", "Res"],
37
+ 9: ["HP", "Str", "Mag", "Skl", "Spd", "Lck", "Def", "Res"],
38
+ }
39
+
40
+ NUM_TO_NAME = {
41
+ 4: "genealogy-of-the-holy-war",
42
+ 5: "thracia-776",
43
+ 6: "binding-blade",
44
+ 7: "blazing-sword",
45
+ 8: "the-sacred-stones",
46
+ 9: "path-of-radiance",
47
+ }
48
+
49
+ NUM_TO_FULLNAME = {
50
+ 4: "Genealogy of the Holy War",
51
+ 5: "Thracia 776",
52
+ 6: "Sword of Seals",
53
+ 7: "Blazing Sword",
54
+ 8: "The Sacred Stones",
55
+ 9: "Path of Radiance",
56
+ }
57
+
58
+ page_dict = {
59
+ "characters/base-stats": "characters__base_stats",
60
+ "characters/growth-rates": "characters__growth_rates",
61
+ "classes/maximum-stats": "classes__maximum_stats",
62
+ "classes/promotion-gains": "classes__promotion_gains",
63
+ }
64
+
65
+ tables_file = "cleaned_stats.db"
66
+
67
+ @property
68
+ def game_num(self):
69
+ return self._game_num
70
+
71
+ @property
72
+ def game_name(self):
73
+ return self._game_name
74
+
75
+ @property
76
+ def history(self):
77
+ return self._history
78
+
79
+ @property
80
+ def comparison_labels(self):
81
+ return self._comparison_labels
82
+
83
+ @property
84
+ def home_dir(self):
85
+ return self._home_dir
86
+
87
+ def __init__(self, game_num: int):
88
+ """
89
+ Defines:
90
+ current_clstype - For use in cross-referencing names in tables.
91
+ current_cls - Stores current class
92
+ target_stats - Temporary store for stat-augmenting arrays
93
+ """
94
+
95
+ """ SerenesCleaner
96
+ self.fieldrecon_file = "fieldrecon.json" # not needed
97
+ self.clsrecon_list = [
98
+ (("characters/base-stats", "Name", "Name"), ("characters/growth-rates", "Name")),
99
+ (("characters/base-stats", "Name", "Class"), ("classes/maximum-stats", "Class")),
100
+ (("characters/base-stats", "Name", "Class"), ("classes/promotion-gains", "Class")),
101
+ (("classes/promotion-gains", "Promotion", "Promotion"), ("classes/maximum-stats", "Class")),
102
+ (("classes/promotion-gains", "Promotion", "Promotion"), ("classes/promotion-gains", "Class")),
103
+ ] # not needed
104
+ """
105
+
106
+ """ SerenesTranscriber
107
+ # To be edited never
108
+ self.tables_file = "cleaned_stats.db"
109
+ """
110
+
111
+ # To be edited only by program
112
+ self.url_to_tables = {}
113
+ """ SerenesScraper
114
+ _URL_ROOT = "https://serenesforest.net" # not needed
115
+ self.URL_ROOT = "https://serenesforest.net" # not needed
116
+ """
117
+
118
+ """ SerenesBase
119
+ self._NUM_TO_NAME = { # not needed
120
+ self.NUM_TO_NAME = {
121
+ 4: "genealogy-of-the-holy-war",
122
+ 5: "thracia-776",
123
+ 6: "binding-blade",
124
+ 7: "blazing-sword",
125
+ 8: "the-sacred-stones",
126
+ 9: "path-of-radiance",
127
+ } # not needed
128
+ """
129
+ # To be edited never
130
+ self._game_num = game_num
131
+ # To be edited never
132
+ self._game_name = self.NUM_TO_NAME[game_num]
133
+ # essential to set_targetstats method
134
+ # TODO: To be edited only by program. Is set by `set_targetstats` method.
135
+ # How to restrict permissions on this field so that it can only be modified by `set_targetstats`?
136
+ # Perhaps a pd-Series equivalent of list.append?
137
+ self.target_stats = None
138
+ # Can be edited by user.
139
+ self.current_stats = None
140
+ # To be edited only by program
141
+ # history (class, lv) tuples, and labels for comparison DataFrame
142
+ self._history = []
143
+ # To be edited only by program
144
+ self._comparison_labels = {}
145
+ # load tables
146
+ # To be edited never
147
+ self._home_dir = Path(self.DATADIR_ROOT).joinpath(self.game_name)
148
+ # To be edited by user. Turned into class attribute.
149
+ #self.tables_file = "cleaned_stats.db"
150
+
151
+ def load_tables(self, urlpath: str):
152
+ """
153
+ Loads the table-list into url_to_tables[urlpath] from home_dir/tables_file.
154
+
155
+ Raises:
156
+ - FileNotFoundError: tables_file does not exist.
157
+ - KeyError: urlpath is not registered in page_dict.
158
+ """
159
+ logging.info("SerenesTranscriber.load_tables(self, '%s')", urlpath)
160
+ load_path = self.home_dir.joinpath(self.tables_file)
161
+ if not load_path.exists():
162
+ raise FileNotFoundError(f"'{str(load_path)}' does not exist. Aborting.")
163
+ load_file = str(load_path)
164
+ tablename_root = self.page_dict[urlpath]
165
+ logging.info("SerenesTranscriber.url_to_tables['%s'] = []", urlpath)
166
+ self.url_to_tables[urlpath] = []
167
+ tableindex = 0
168
+ logging.info("Loading tables into SerenesTranscriber.url_to_tables['%s'].", urlpath)
169
+ while True:
170
+ table_name = tablename_root + str(tableindex)
171
+ con = "sqlite:///" + load_file
172
+ try:
173
+ logging.info("pd.read_sql_table('%s', '%s')", table_name, con)
174
+ table = pd.read_sql_table(table_name, con)
175
+ tableindex += 1
176
+ logging.info("SerenesTranscriber.url_to_tables['%s'].append(tables[%d])", urlpath, tableindex-1)
177
+ self.url_to_tables[urlpath].append(table)
178
+ except ValueError:
179
+ logging.info("%d table(s) have been loaded into SerenesTranscriber.url_to_tables['%s']", tableindex, urlpath)
180
+ break
181
+
182
+
183
+ class BaseMorph(ProtoMorph):
184
+ """
185
+ """
186
+
187
+ url_to_tables = {}
188
+
189
+ def __init__(self, game_num: int):
190
+ ProtoMorph.__init__(self, game_num)
191
+ # initialize instance attribute `url_to_tables`
192
+ del self.url_to_tables
193
+ # delete instance attribute `url_to_tables`, and allow class attribute `url_to_tables` to occupy namespace
194
+ #if not self.url_to_tables:
195
+ for urlpath in self.page_dict:
196
+ # TODO: Find out why this generates an error
197
+ #if urlpath in self.url_to_tables: continue
198
+ """
199
+ FAILED tests/integration/test_morph__integration.py::Morph6IntegrationTest::test_fe8_units - KeyError: 'Non-promoted (foot)'
200
+ FAILED tests/integration/test_morph__integration.py::Morph6IntegrationTest::test_fe9_units - KeyError: 'Non-promoted physical'
201
+ FAILED tests/integration/test_morph__integration.py::Morph6IntegrationTest::test_villager_amelia - KeyError: 'Non-promoted (foot)'
202
+ FAILED tests/integration/test_morph__integration.py::Morph6IntegrationTest::test_villager_ewan - KeyError: 'Non-promoted (foot)'
203
+ FAILED tests/integration/test_morph__integration.py::Morph6IntegrationTest::test_villager_ross - KeyError: 'Non-promoted (foot)'
204
+ FAILED tests/integration/test_morph__integration.py::Morph7IntegrationTest::test_all_units - KeyError: 'Wallace'
205
+ FAILED tests/integration/test_morph__integration.py::Morph7IntegrationTest::test_main_wallace - KeyError: 'Wallace'
206
+ FAILED tests/integration/test_morph__integration.py::Morph7IntegrationTest::test_tutorial_wallace - KeyError: 'Wallace'
207
+ FAILED tests/integration/test_morph__integration.py::Morph9IntegrationTest::test_all_units - KeyError: 'Non-promoted physical'
208
+ FAILED tests/unit/test__basemorph.py::BaseMorphTest::test_get_character_list - AssertionError: False is not true
209
+ FAILED tests/unit/test__basemorph.py::BaseMorphTest::test_set_targetstats - KeyError: 'Fir'
210
+ FAILED tests/unit/test_morph.py::Morph6Test::test_level_up - KeyError: 'Roy'
211
+ FAILED tests/unit/test_morph.py::Morph7Test::test__lt__ - KeyError: 'Wallace'
212
+ FAILED tests/unit/test_morph.py::Morph7Test::test__lt__HM - KeyError: 'Wallace'
213
+ FAILED tests/unit/test_morph.py::Morph7Test::test__repr__ - KeyError: 'Wallace'
214
+ FAILED tests/unit/test_morph.py::Morph7Test::test_get_repr_series__comparison_labels - KeyError: 'Wallace'
215
+ """
216
+ self.load_tables(urlpath)
217
+
218
+ def verify_clsrecon_file(self, ltable_args: Tuple[str, str, str], rtable_args: Tuple[str, str]):
219
+ """
220
+ Prints: clsrecon_dict.keys not in ltable[lindex_col], clsrecon_dict.values not in rtable[to_col].
221
+
222
+ Note: In order for this method to work, logging.level must be set to logging.INFO.
223
+ Find cls-recon list in unittest.TestCase subclass for BaseMorph.
224
+ """
225
+ logging.info("BaseMorph.verify_clsrecon_file(self, %s, %s)", ltable_args, rtable_args)
226
+ # unpack arguments
227
+ ltable_url, lindex_col, from_col = ltable_args
228
+ rtable_url, to_col = rtable_args
229
+ # load clsrecon_dict
230
+ ltable_name = self.page_dict[ltable_url]
231
+ rtable_name = self.page_dict[rtable_url]
232
+ json_path = self.home_dir.joinpath(f"{ltable_name}-JOIN-{rtable_name}.json")
233
+ with io.open(str(json_path), encoding='utf-8') as rfile:
234
+ clsrecon_dict = json.load(rfile)
235
+ # compile ltable pkey names
236
+ ltable_nameset = set()
237
+ for table in self.url_to_tables[ltable_url]:
238
+ ltable_nameset.update(set(table.loc[:, lindex_col]))
239
+ # compile rtable values
240
+ rtable_nameset = set()
241
+ for table in self.url_to_tables[rtable_url]:
242
+ rtable_nameset.update(set(table.loc[:, to_col]))
243
+ # check1: clsrecon_dict.keys subset of ltable_nameset
244
+ check1 = set(clsrecon_dict) - ltable_nameset
245
+ logging.info("%d key(s) in '%s', but not in '%s.%s': %s", len(check1), str(json_path), ltable_name, lindex_col, check1)
246
+ # check2: clsrecon_dict.values subset of rtable_nameset
247
+ check2 = set(clsrecon_dict.values()) - {None} - rtable_nameset
248
+ logging.info("%d value(s) in '%s', but not in '%s.%s': %s", len(check2), str(json_path), rtable_name, to_col, check2)
249
+
250
+ def verify_maximum_stats(self):
251
+ """
252
+ Prints columns that are in 'classes/maximum-stats' but not in 'characters/base-stats'.
253
+
254
+ Note: In order for this method to work, logging.level must be set to logging.INFO.
255
+ """
256
+ logging.info("BaseMorph.verify_maximum_stats(self)")
257
+ bases_name = "characters/base-stats"
258
+ maxes_name = "classes/maximum-stats"
259
+ bases = set(self.url_to_tables[bases_name][0].columns) - {"Name", "Class", "Lv"}
260
+ maxes = set(self.url_to_tables[maxes_name][0].columns) - {"Class"}
261
+ diff = maxes - bases
262
+ logging.info("%d names in '%s' but not in '%s': %s", len(diff), maxes_name, bases_name, diff)
263
+
264
+ def set_targetstats(self, ltable_args: Tuple[str, str], rtable_args: Tuple[str, str], tableindex: int):
265
+ """
266
+ Sets BaseMorph.target_stats to the pd.[DataFrame|Series] per clsrecon_dict.
267
+
268
+ ltable_url, lindex_col = ltable_args
269
+ rtable_url, to_col = rtable_args
270
+ lindex_col |-(clsrecon_dict)-> from_col === to_col
271
+ """
272
+ logging.info("BaseMorph.set_targetstats(self, %s, %s)", ltable_args, rtable_args)
273
+ # unpack arguments
274
+ ltable_url, lindex_val = ltable_args
275
+ rtable_url, to_col = rtable_args
276
+ # load clsrecon_dict from json
277
+ ltable_name = self.page_dict[ltable_url]
278
+ rtable_name = self.page_dict[rtable_url]
279
+ json_path = self.home_dir.joinpath(f"{ltable_name}-JOIN-{rtable_name}.json")
280
+ with open(str(json_path), encoding='utf-8') as rfile:
281
+ clsrecon_dict = json.load(rfile)
282
+ from_col = clsrecon_dict[lindex_val]
283
+ if from_col is None:
284
+ self.target_stats = None
285
+ else:
286
+ self.target_stats = self.url_to_tables[rtable_url][tableindex].set_index(to_col).loc[from_col, :]
287
+
288
+ def get_lynmode_options(self) -> List[str]:
289
+ """
290
+ Returns a List[str] of possible pseudo-booleans for a member of the Lyndis League.
291
+ """
292
+ lyn_options = [False, True]
293
+ return [str(option) for option in lyn_options]
294
+
295
+ def get_hardmode_options(self) -> List[str]:
296
+ """
297
+ Returns a List[str] of possible pseudo-booleans for a character with Hard Mode bonuses.
298
+ """
299
+ hm_options = [False, True]
300
+ return [str(option) for option in hm_options]
301
+
302
+ def get_character_list(self) -> List[str]:
303
+ """
304
+ Returns a List[str] of base!character names mapped to growths!character names.
305
+
306
+ Raises:
307
+ - FileNotFoundError: not home_dir.joinpath("characters__base_stats-JOIN-characters__growth_rates.json").exists()
308
+ - json.decoder.JSONDecodeError: File is not in JSON form.
309
+ """
310
+ logging.info("BaseMorph.get_character_list(self)")
311
+ ltable_url = "characters/base-stats"
312
+ rtable_url = "characters/growth-rates"
313
+ ltable_name = self.page_dict[ltable_url]
314
+ rtable_name = self.page_dict[rtable_url]
315
+ json_path = self.home_dir.joinpath(f"{ltable_name}-JOIN-{rtable_name}.json")
316
+ # query character list from JSON (FileNotFoundError may be raised before that)
317
+ with io.open(str(json_path), encoding='utf-8') as rfile:
318
+ clsrecon_dict = json.load(rfile)
319
+ chrlist = []
320
+ # filter to include non-None entries
321
+ for chrname, growth_equivalent in clsrecon_dict.items():
322
+ if growth_equivalent is None:
323
+ continue
324
+ chrlist.append(chrname)
325
+ # return keys mapped to non-None
326
+ logging.info("%d names found: %s", len(chrlist), chrlist)
327
+ return chrlist
328
+
329
+ def get_fe4_unit_list(self, unit_type: str) -> List[str]:
330
+ """
331
+ Shortcut function to get a list of either FE4 kids or fathers.
332
+ """
333
+ assert self.game_num == 4
334
+ tgt_colname = {
335
+ "kid": "Name",
336
+ "father": "Father",
337
+ }[unit_type]
338
+ tablename = "characters/growth-rates"
339
+ src_tablelist = self.url_to_tables[tablename]
340
+ src_table = src_tablelist[1]
341
+ src_column = src_table[tgt_colname].copy()
342
+ src_column.drop_duplicates(inplace=True)
343
+ unit_list = src_column.to_list()
344
+ return unit_list
345
+