tibiawikisql 7.0.2__tar.gz → 8.0.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 (71) hide show
  1. {tibiawikisql-7.0.2/tibiawikisql.egg-info → tibiawikisql-8.0.0}/PKG-INFO +4 -4
  2. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/README.md +3 -3
  3. tibiawikisql-8.0.0/tests/test_generation.py +301 -0
  4. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tests/tests_utils.py +54 -1
  5. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/__init__.py +1 -1
  6. tibiawikisql-8.0.0/tibiawikisql/__main__.py +49 -0
  7. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/database.py +1 -1
  8. tibiawikisql-8.0.0/tibiawikisql/generation.py +349 -0
  9. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/__init__.py +2 -2
  10. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/creature.py +1 -1
  11. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/imbuement.py +1 -1
  12. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/item.py +30 -2
  13. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/npc.py +1 -89
  14. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/outfit.py +1 -1
  15. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/quest.py +1 -1
  16. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/spell.py +2 -42
  17. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/creature.py +2 -3
  18. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/item.py +2 -1
  19. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/spell.py +0 -2
  20. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/schema.py +10 -70
  21. tibiawikisql-8.0.0/tibiawikisql/tasks/__init__.py +2 -0
  22. tibiawikisql-8.0.0/tibiawikisql/tasks/images.py +268 -0
  23. tibiawikisql-8.0.0/tibiawikisql/tasks/item_offers.py +107 -0
  24. tibiawikisql-8.0.0/tibiawikisql/tasks/item_proficiency_perks.py +96 -0
  25. tibiawikisql-8.0.0/tibiawikisql/tasks/loot_statistics.py +73 -0
  26. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/utils.py +80 -0
  27. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0/tibiawikisql.egg-info}/PKG-INFO +4 -4
  28. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql.egg-info/SOURCES.txt +6 -1
  29. tibiawikisql-7.0.2/tests/test_generation.py +0 -124
  30. tibiawikisql-7.0.2/tibiawikisql/__main__.py +0 -34
  31. tibiawikisql-7.0.2/tibiawikisql/generation.py +0 -684
  32. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/LICENSE +0 -0
  33. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/MANIFEST.in +0 -0
  34. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/pyproject.toml +0 -0
  35. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/requirements-docs.txt +0 -0
  36. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/requirements-server.txt +0 -0
  37. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/requirements-testing.txt +0 -0
  38. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/requirements.txt +0 -0
  39. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/setup.cfg +0 -0
  40. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tests/tests_models.py +0 -0
  41. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tests/tests_parsers.py +0 -0
  42. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tests/tests_schema.py +0 -0
  43. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tests/tests_wikiapi.py +0 -0
  44. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/api.py +0 -0
  45. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/errors.py +0 -0
  46. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/achievement.py +0 -0
  47. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/base.py +0 -0
  48. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/charm.py +0 -0
  49. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/house.py +0 -0
  50. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/mount.py +0 -0
  51. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/update.py +0 -0
  52. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/models/world.py +0 -0
  53. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/__init__.py +0 -0
  54. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/achievement.py +0 -0
  55. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/base.py +0 -0
  56. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/book.py +0 -0
  57. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/charm.py +0 -0
  58. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/house.py +0 -0
  59. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/imbuement.py +0 -0
  60. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/key.py +0 -0
  61. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/mount.py +0 -0
  62. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/npc.py +0 -0
  63. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/outfit.py +0 -0
  64. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/quest.py +0 -0
  65. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/update.py +0 -0
  66. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/parsers/world.py +0 -0
  67. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql/server.py +0 -0
  68. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql.egg-info/dependency_links.txt +0 -0
  69. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql.egg-info/entry_points.txt +0 -0
  70. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql.egg-info/requires.txt +0 -0
  71. {tibiawikisql-7.0.2 → tibiawikisql-8.0.0}/tibiawikisql.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tibiawikisql
3
- Version: 7.0.2
3
+ Version: 8.0.0
4
4
  Summary: Python script that generates a SQLite database from TibiaWiki articles
5
5
  Author-email: Allan Galarza <allan.galarza@gmail.com>
6
6
  Maintainer-email: Allan Galarza <allan.galarza@gmail.com>
@@ -109,8 +109,8 @@ If a newer version of the image has been uploaded, it will be updated.
109
109
  When done, a database file called `tibiawiki.db` will be found on the folder.
110
110
 
111
111
  ## Docker
112
- ![Docker Pulls](https://img.shields.io/docker/pulls/galarzaa90/tibiawiki-sql)
113
- ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/galarzaa90/tibiawiki-sql?sort=semver)
112
+ [![Docker Pulls](https://img.shields.io/docker/pulls/galarzaa90/tibiawiki-sql)](https://hub.docker.com/r/galarzaa90/tibiawiki-sql)
113
+ [![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/galarzaa90/tibiawiki-sql?sort=semver)](https://hub.docker.com/r/galarzaa90/tibiawiki-sql/tags)
114
114
 
115
115
  The database can also be generated without installing the project, it's dependencies, or Python, by using Docker.
116
116
  Make sure to have Docker installed, then simply run:
@@ -149,4 +149,4 @@ For questions, suggestions and bug reports, submit an issue.
149
149
 
150
150
  The best way to contribute to this project is by contributing to [TibiaWiki](https://tibia.fandom.com).
151
151
 
152
- ![image](https://vignette.wikia.nocookie.net/tibia/images/d/d9/Tibiawiki_Small.gif/revision/latest?cb=20150129101832&path-prefix=en)
152
+ [![image](https://vignette.wikia.nocookie.net/tibia/images/d/d9/Tibiawiki_Small.gif/revision/latest?cb=20150129101832&path-prefix=en)](https://tibia.fandom.com/)
@@ -54,8 +54,8 @@ If a newer version of the image has been uploaded, it will be updated.
54
54
  When done, a database file called `tibiawiki.db` will be found on the folder.
55
55
 
56
56
  ## Docker
57
- ![Docker Pulls](https://img.shields.io/docker/pulls/galarzaa90/tibiawiki-sql)
58
- ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/galarzaa90/tibiawiki-sql?sort=semver)
57
+ [![Docker Pulls](https://img.shields.io/docker/pulls/galarzaa90/tibiawiki-sql)](https://hub.docker.com/r/galarzaa90/tibiawiki-sql)
58
+ [![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/galarzaa90/tibiawiki-sql?sort=semver)](https://hub.docker.com/r/galarzaa90/tibiawiki-sql/tags)
59
59
 
60
60
  The database can also be generated without installing the project, it's dependencies, or Python, by using Docker.
61
61
  Make sure to have Docker installed, then simply run:
@@ -94,4 +94,4 @@ For questions, suggestions and bug reports, submit an issue.
94
94
 
95
95
  The best way to contribute to this project is by contributing to [TibiaWiki](https://tibia.fandom.com).
96
96
 
97
- ![image](https://vignette.wikia.nocookie.net/tibia/images/d/d9/Tibiawiki_Small.gif/revision/latest?cb=20150129101832&path-prefix=en)
97
+ [![image](https://vignette.wikia.nocookie.net/tibia/images/d/d9/Tibiawiki_Small.gif/revision/latest?cb=20150129101832&path-prefix=en)](https://tibia.fandom.com/)
@@ -0,0 +1,301 @@
1
+ import datetime
2
+ import sqlite3
3
+ import unittest
4
+ from unittest.mock import Mock, patch
5
+
6
+ from click.testing import CliRunner
7
+
8
+ from tests import load_resource
9
+ from tibiawikisql import __main__ as cli_module
10
+ from tibiawikisql import generation as generation_module
11
+ from tibiawikisql.api import Article
12
+ from tibiawikisql.generation import WEAPON_PROFICIENCY_NAME_ARTICLE, WEAPON_PROFICIENCY_TABLES_ARTICLE
13
+ from tibiawikisql.schema import ItemProficiencyPerkTable, ItemTable
14
+ from tibiawikisql.tasks.item_proficiency_perks import generate_item_proficiency_perks
15
+ from tibiawikisql.tasks.loot_statistics import generate_loot_statistics
16
+
17
+
18
+ class TestGeneration(unittest.TestCase):
19
+ def setUp(self):
20
+ self.conn = sqlite3.connect(":memory:")
21
+ self.conn.executescript(ItemTable.get_create_table_statement())
22
+ self.conn.executescript(ItemProficiencyPerkTable.get_create_table_statement())
23
+ timestamp = datetime.datetime.fromisoformat("2024-01-01T00:00:00+00:00")
24
+ ItemTable.insert(self.conn, article_id=1, title="Amber Axe", timestamp=timestamp)
25
+ ItemTable.insert(self.conn, article_id=2, title="Amber Cudgel", timestamp=timestamp)
26
+
27
+ def tearDown(self):
28
+ self.conn.close()
29
+
30
+ @staticmethod
31
+ def build_article(title: str, content: str) -> Article:
32
+ return Article(
33
+ article_id=9999,
34
+ title=title,
35
+ timestamp=datetime.datetime.fromisoformat("2024-01-01T00:00:00+00:00"),
36
+ content=content,
37
+ )
38
+
39
+ def test_generate_item_proficiency_perks(self):
40
+ mapping_article = self.build_article(
41
+ WEAPON_PROFICIENCY_NAME_ARTICLE,
42
+ load_resource("content_weapon_proficiency_name.txt"),
43
+ )
44
+ tables_article = self.build_article(
45
+ WEAPON_PROFICIENCY_TABLES_ARTICLE,
46
+ load_resource("content_weapon_proficiency_tables.txt"),
47
+ )
48
+ data_store = {"items_map": {"amber axe": 1, "amber cudgel": 2}}
49
+
50
+ wiki_client = Mock()
51
+ wiki_client.get_article.side_effect = [mapping_article, tables_article]
52
+ mock_echo = Mock()
53
+ generate_item_proficiency_perks(
54
+ self.conn,
55
+ data_store,
56
+ wiki_client=wiki_client,
57
+ mapping_article_title=WEAPON_PROFICIENCY_NAME_ARTICLE,
58
+ tables_article_title=WEAPON_PROFICIENCY_TABLES_ARTICLE,
59
+ timed=generation_module.timed,
60
+ echo=mock_echo,
61
+ )
62
+
63
+ rows = self.conn.execute(
64
+ "SELECT item_id, proficiency_level, skill_image, icon, effect "
65
+ "FROM item_proficiency_perk ORDER BY rowid",
66
+ ).fetchall()
67
+ self.assertEqual(4, len(rows))
68
+ self.assertEqual(
69
+ [
70
+ (1, 1, "Axe Skill Bonus", None, "+1 Axe Fighting"),
71
+ (1, 2, "Auto-Attack Critical Extra Damage", None, "+10% critical extra damage for auto-attacks"),
72
+ (
73
+ 1,
74
+ 2,
75
+ "Axe Extra Damage Auto-Attack",
76
+ "Special Icon",
77
+ "+5% of your Axe Fighting as extra damage for auto-attacks",
78
+ ),
79
+ (2, 1, "Club Skill Bonus", None, "+1 Club Fighting"),
80
+ ],
81
+ [tuple(row) for row in rows],
82
+ )
83
+ messages = " ".join(call.args[0] for call in mock_echo.call_args_list if call.args)
84
+ self.assertIn("Parsed weapon proficiency perks", messages)
85
+
86
+ def test_generate_item_proficiency_perks_warn_and_continue(self):
87
+ mapping_article = self.build_article(
88
+ WEAPON_PROFICIENCY_NAME_ARTICLE,
89
+ """{{#switch:{{{1|}}}
90
+ | Amber Axe = Sanguine 1H Axe
91
+ | Amber Cudgel = Missing Section
92
+ | Unknown Item = Sanguine 1H Club
93
+ |#default =
94
+ }}""",
95
+ )
96
+ tables_article = self.build_article(
97
+ WEAPON_PROFICIENCY_TABLES_ARTICLE,
98
+ load_resource("content_weapon_proficiency_tables.txt"),
99
+ )
100
+ data_store = {"items_map": {"amber axe": 1, "amber cudgel": 2}}
101
+
102
+ wiki_client = Mock()
103
+ wiki_client.get_article.side_effect = [mapping_article, tables_article]
104
+ mock_echo = Mock()
105
+ generate_item_proficiency_perks(
106
+ self.conn,
107
+ data_store,
108
+ wiki_client=wiki_client,
109
+ mapping_article_title=WEAPON_PROFICIENCY_NAME_ARTICLE,
110
+ tables_article_title=WEAPON_PROFICIENCY_TABLES_ARTICLE,
111
+ timed=generation_module.timed,
112
+ echo=mock_echo,
113
+ )
114
+
115
+ rows = self.conn.execute(
116
+ "SELECT item_id, proficiency_level, skill_image, icon, effect FROM item_proficiency_perk",
117
+ ).fetchall()
118
+ self.assertEqual(3, len(rows))
119
+ messages = " ".join(call.args[0] for call in mock_echo.call_args_list if call.args)
120
+ self.assertIn("Could not map 1 item names to IDs", messages)
121
+ self.assertIn("Could not find 1 proficiency sections", messages)
122
+ self.assertIn("Skipped 1 malformed weapon proficiency perks", messages)
123
+
124
+ def test_generate_item_proficiency_perks_no_title_fallback(self):
125
+ mapping_article = self.build_article(
126
+ WEAPON_PROFICIENCY_NAME_ARTICLE,
127
+ load_resource("content_weapon_proficiency_name.txt"),
128
+ )
129
+ data_store = {"items_map": {"amber axe": 1, "amber cudgel": 2}}
130
+
131
+ def get_article_side_effect(title: str) -> Article | None:
132
+ if title == WEAPON_PROFICIENCY_NAME_ARTICLE:
133
+ return mapping_article
134
+ return None
135
+
136
+ wiki_client = Mock()
137
+ wiki_client.get_article.side_effect = get_article_side_effect
138
+ mock_echo = Mock()
139
+ generate_item_proficiency_perks(
140
+ self.conn,
141
+ data_store,
142
+ wiki_client=wiki_client,
143
+ mapping_article_title=WEAPON_PROFICIENCY_NAME_ARTICLE,
144
+ tables_article_title=WEAPON_PROFICIENCY_TABLES_ARTICLE,
145
+ timed=generation_module.timed,
146
+ echo=mock_echo,
147
+ )
148
+
149
+ rows = self.conn.execute("SELECT COUNT(*) FROM item_proficiency_perk").fetchone()
150
+ self.assertEqual(0, rows[0])
151
+ self.assertEqual(
152
+ [
153
+ WEAPON_PROFICIENCY_NAME_ARTICLE,
154
+ WEAPON_PROFICIENCY_TABLES_ARTICLE,
155
+ ],
156
+ [call.args[0] for call in wiki_client.get_article.call_args_list],
157
+ )
158
+ messages = " ".join(call.args[0] for call in mock_echo.call_args_list if call.args)
159
+ self.assertIn("Could not fetch weapon proficiency pages", messages)
160
+ self.assertIn(WEAPON_PROFICIENCY_TABLES_ARTICLE, messages)
161
+
162
+ def test_generate_item_proficiency_perks_case_insensitive_section_match(self):
163
+ timestamp = datetime.datetime.fromisoformat("2024-01-01T00:00:00+00:00")
164
+ ItemTable.insert(self.conn, article_id=3, title="Stale Bread of Ancientness", timestamp=timestamp)
165
+
166
+ mapping_article = self.build_article(
167
+ WEAPON_PROFICIENCY_NAME_ARTICLE,
168
+ """{{#switch:{{{1|}}}
169
+ | Stale Bread of Ancientness = Club 1H Stale Bread of Ancientness
170
+ |#default =
171
+ }}""",
172
+ )
173
+ tables_article = self.build_article(
174
+ WEAPON_PROFICIENCY_TABLES_ARTICLE,
175
+ """===Club 1H Stale Bread Of Ancientness===
176
+ {{Weapon Proficiency Table
177
+ |perk_1 =
178
+ {{Weapon Proficiency Button |skill_image=Club Skill Bonus|icon=|text=+1 Club Fighting}}
179
+ }}""",
180
+ )
181
+ data_store = {"items_map": {"stale bread of ancientness": 3}}
182
+
183
+ wiki_client = Mock()
184
+ wiki_client.get_article.side_effect = [mapping_article, tables_article]
185
+ generate_item_proficiency_perks(
186
+ self.conn,
187
+ data_store,
188
+ wiki_client=wiki_client,
189
+ mapping_article_title=WEAPON_PROFICIENCY_NAME_ARTICLE,
190
+ tables_article_title=WEAPON_PROFICIENCY_TABLES_ARTICLE,
191
+ timed=generation_module.timed,
192
+ echo=Mock(),
193
+ )
194
+
195
+ rows = self.conn.execute(
196
+ "SELECT item_id, proficiency_level, skill_image, icon, effect FROM item_proficiency_perk",
197
+ ).fetchall()
198
+ self.assertEqual([(3, 1, "Club Skill Bonus", None, "+1 Club Fighting")], [tuple(row) for row in rows])
199
+
200
+
201
+ class TestGenerationOrchestration(unittest.TestCase):
202
+ def setUp(self):
203
+ self.conn = sqlite3.connect(":memory:")
204
+
205
+ def tearDown(self):
206
+ self.conn.close()
207
+
208
+ def test_skip_category_excludes_fetch_and_parse(self):
209
+ with (
210
+ patch("tibiawikisql.generation.fetch_category_entries", return_value=[]) as mock_fetch,
211
+ patch.object(generation_module.wiki_client, "get_articles", return_value=[]) as mock_get_articles,
212
+ patch("tibiawikisql.generation.POST_TASKS", ()),
213
+ ):
214
+ generation_module.generate(self.conn, skip_categories=("achievements",))
215
+
216
+ fetched_categories = [call.args[0] for call in mock_fetch.call_args_list]
217
+ self.assertNotIn("Achievements", fetched_categories)
218
+ self.assertEqual(len(generation_module.CATEGORIES) - 1, len(mock_get_articles.call_args_list))
219
+
220
+ def test_hard_dependencies_auto_skip_categories(self):
221
+ with (
222
+ patch("tibiawikisql.generation.fetch_category_entries", return_value=[]) as mock_fetch,
223
+ patch.object(generation_module.wiki_client, "get_articles", return_value=[]),
224
+ patch("tibiawikisql.generation.POST_TASKS", ()),
225
+ patch("tibiawikisql.generation.click.echo") as mock_echo,
226
+ ):
227
+ generation_module.generate(self.conn, skip_categories=("items",))
228
+
229
+ fetched_categories = [call.args[0] for call in mock_fetch.call_args_list]
230
+ self.assertNotIn("Objects", fetched_categories)
231
+ self.assertNotIn("Keys", fetched_categories)
232
+ messages = " ".join(call.args[0] for call in mock_echo.call_args_list if call.args)
233
+ self.assertIn("Skipping category 'keys'", messages)
234
+
235
+ def test_post_tasks_are_gated_by_enabled_categories(self):
236
+ gated_task = Mock()
237
+ always_task = Mock()
238
+ post_tasks = (
239
+ generation_module.PostTask("needs_items", lambda *_: gated_task(), dependencies=("items",)),
240
+ generation_module.PostTask("always_runs", lambda *_: always_task()),
241
+ )
242
+ with (
243
+ patch("tibiawikisql.generation.fetch_category_entries", return_value=[]),
244
+ patch.object(generation_module.wiki_client, "get_articles", return_value=[]),
245
+ patch("tibiawikisql.generation.POST_TASKS", post_tasks),
246
+ patch("tibiawikisql.generation.click.echo") as mock_echo,
247
+ ):
248
+ generation_module.generate(self.conn, skip_categories=("items",))
249
+
250
+ gated_task.assert_not_called()
251
+ always_task.assert_called_once_with()
252
+ messages = " ".join(call.args[0] for call in mock_echo.call_args_list if call.args)
253
+ self.assertIn("Skipping task 'needs_items'", messages)
254
+
255
+ def test_generate_loot_statistics_early_return_without_maps(self):
256
+ wiki_client = Mock()
257
+ generate_loot_statistics(
258
+ self.conn,
259
+ {},
260
+ wiki_client=wiki_client,
261
+ progress_bar=generation_module.progress_bar,
262
+ article_label=generation_module.article_label,
263
+ timed=generation_module.timed,
264
+ echo=Mock(),
265
+ )
266
+ wiki_client.get_articles.assert_not_called()
267
+
268
+
269
+ class TestGenerateCommand(unittest.TestCase):
270
+ def setUp(self):
271
+ self.runner = CliRunner()
272
+
273
+ def test_skip_category_option_is_passed_to_generate(self):
274
+ with patch("tibiawikisql.__main__.generation.generate") as mock_generate:
275
+ result = self.runner.invoke(
276
+ cli_module.cli,
277
+ [
278
+ "generate",
279
+ "--db-name",
280
+ ":memory:",
281
+ "--skip-category",
282
+ "achievements",
283
+ "--skip-category",
284
+ "items",
285
+ ],
286
+ )
287
+ self.assertEqual(0, result.exit_code, result.output)
288
+ self.assertEqual(("achievements", "items"), mock_generate.call_args.kwargs["skip_categories"])
289
+
290
+ def test_skip_category_rejects_invalid_value(self):
291
+ result = self.runner.invoke(
292
+ cli_module.cli,
293
+ [
294
+ "generate",
295
+ "--skip-category",
296
+ "invalid-category",
297
+ ],
298
+ )
299
+ self.assertNotEqual(0, result.exit_code)
300
+ self.assertIn("Invalid value", result.output)
301
+ self.assertIn("--skip-category", result.output)
@@ -2,7 +2,8 @@ import unittest
2
2
 
3
3
  from tests import load_resource
4
4
  from tibiawikisql.utils import (clean_links, client_color_to_rgb, parse_boolean, parse_float, parse_integer,
5
- parse_loot_statistics, parse_min_max, parse_sounds)
5
+ parse_loot_statistics, parse_min_max, parse_sounds,
6
+ parse_weapon_proficiency_name, parse_weapon_proficiency_tables)
6
7
 
7
8
 
8
9
  class TestUtils(unittest.TestCase):
@@ -81,3 +82,55 @@ class TestUtils(unittest.TestCase):
81
82
  self.assertEqual(client_color_to_rgb(3), 0x99)
82
83
  self.assertEqual(client_color_to_rgb(215), 0xffffff)
83
84
  self.assertEqual(client_color_to_rgb(216), 0)
85
+
86
+ def test_parse_weapon_proficiency_name(self):
87
+ content = load_resource("content_weapon_proficiency_name.txt")
88
+ mappings = parse_weapon_proficiency_name(content)
89
+
90
+ self.assertEqual(2, len(mappings))
91
+ self.assertEqual("Sanguine 1H Axe", mappings["Amber Axe"])
92
+ self.assertEqual("Sanguine 1H Club", mappings["Amber Cudgel"])
93
+ self.assertNotIn("#default", mappings)
94
+
95
+ def test_parse_weapon_proficiency_name_without_switch(self):
96
+ mappings = parse_weapon_proficiency_name("== Not a switch ==")
97
+ self.assertEqual({}, mappings)
98
+
99
+ def test_parse_weapon_proficiency_tables(self):
100
+ content = load_resource("content_weapon_proficiency_tables.txt")
101
+ tables = parse_weapon_proficiency_tables(content)
102
+
103
+ self.assertIn("Sanguine 1H Axe", tables)
104
+ self.assertIn("Sanguine 1H Club", tables)
105
+ self.assertIn(1, tables["Sanguine 1H Axe"])
106
+ self.assertIn(2, tables["Sanguine 1H Axe"])
107
+
108
+ perks = tables["Sanguine 1H Axe"][2]
109
+ self.assertEqual(3, len(perks))
110
+ self.assertIsNone(perks[0]["icon"])
111
+ self.assertEqual("Special Icon", perks[1]["icon"])
112
+
113
+ def test_parse_weapon_proficiency_tables_effect_fallback(self):
114
+ content = """===Sanguine 1H Axe===
115
+ {{Weapon Proficiency Table
116
+ |perk_1 =
117
+ {{Weapon Proficiency Button |skill_image=Axe Skill Bonus|icon=|effect=Fallback Effect}}
118
+ |perk_x =
119
+ {{Weapon Proficiency Button |skill_image=Ignored|icon=|text=Ignored}}
120
+ }}"""
121
+ tables = parse_weapon_proficiency_tables(content)
122
+ self.assertIn("Sanguine 1H Axe", tables)
123
+ self.assertIn(1, tables["Sanguine 1H Axe"])
124
+ self.assertEqual([1], list(tables["Sanguine 1H Axe"].keys()))
125
+ self.assertEqual("Fallback Effect", tables["Sanguine 1H Axe"][1][0]["effect"])
126
+
127
+ def test_parse_weapon_proficiency_tables_level_two_headings(self):
128
+ content = """==Sanguine 1H Axe==
129
+ {{Weapon Proficiency Table
130
+ |perk_1 =
131
+ {{Weapon Proficiency Button |skill_image=Axe Skill Bonus|icon=|text=+1 Axe Fighting}}
132
+ }}"""
133
+ tables = parse_weapon_proficiency_tables(content)
134
+ self.assertIn("Sanguine 1H Axe", tables)
135
+ self.assertIn(1, tables["Sanguine 1H Axe"])
136
+ self.assertEqual("+1 Axe Fighting", tables["Sanguine 1H Axe"][1][0]["effect"])
@@ -4,4 +4,4 @@ __author__ = "Allan Galarza"
4
4
  __copyright__ = "Copyright 2025 Allan Galarza"
5
5
 
6
6
  __license__ = "Apache 2.0"
7
- __version__ = "7.0.2"
7
+ __version__ = "8.0.0"
@@ -0,0 +1,49 @@
1
+ import sqlite3
2
+
3
+ import click
4
+ import colorama
5
+
6
+ from tibiawikisql import __version__, generation
7
+ from tibiawikisql.utils import timed
8
+
9
+ DATABASE_FILE = "tibiawiki.db"
10
+
11
+ colorama.init()
12
+
13
+
14
+ @click.group(context_settings={'help_option_names': ['-h', '--help']})
15
+ @click.version_option(__version__, '-V', '--version')
16
+ def cli():
17
+ # Empty command group to disable default command.
18
+ pass
19
+
20
+
21
+ @cli.command(name="generate")
22
+ @click.option('-i', '--skip-images', help="Skip fetching and loading images to the database.", is_flag=True)
23
+ @click.option('-o', '--db-name', help="Name for the database file.", default=DATABASE_FILE)
24
+ @click.option('-d', '--skip-deprecated', help="Skips fetching deprecated articles and their images.", is_flag=True)
25
+ @click.option(
26
+ "-c",
27
+ "--skip-category",
28
+ "skip_categories",
29
+ multiple=True,
30
+ type=click.Choice(sorted(generation.CATEGORIES), case_sensitive=False),
31
+ help=(
32
+ "Skip specific categories. Can be repeated."
33
+ ),
34
+ )
35
+ def generate(skip_images, db_name, skip_deprecated, skip_categories):
36
+ """Generates a database file."""
37
+ with timed() as t:
38
+ with sqlite3.connect(db_name) as conn:
39
+ generation.generate(
40
+ conn,
41
+ skip_images=skip_images,
42
+ skip_deprecated=skip_deprecated,
43
+ skip_categories=skip_categories,
44
+ )
45
+ click.echo(f"Command finished in {t.elapsed:.2f} seconds.")
46
+
47
+
48
+ if __name__ == "__main__":
49
+ cli()
@@ -7,7 +7,7 @@ import sqlite3
7
7
  from sqlite3 import Connection, Cursor, Row
8
8
  from typing import Any, ClassVar, TypeVar
9
9
 
10
- from pypika import Order, Query, Table as PTable
10
+ from pypika import Order, SQLLiteQuery as Query, Table as PTable
11
11
 
12
12
  from tibiawikisql.errors import InvalidColumnValueError, SchemaError
13
13