tibiawikisql 7.0.3__tar.gz → 8.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {tibiawikisql-7.0.3/tibiawikisql.egg-info → tibiawikisql-8.0.1}/PKG-INFO +3 -3
  2. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/pyproject.toml +2 -2
  3. tibiawikisql-8.0.1/tests/test_generation.py +301 -0
  4. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tests/tests_utils.py +59 -1
  5. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/__init__.py +1 -1
  6. tibiawikisql-8.0.1/tibiawikisql/__main__.py +62 -0
  7. tibiawikisql-8.0.1/tibiawikisql/generation.py +398 -0
  8. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/__init__.py +2 -2
  9. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/item.py +26 -0
  10. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/npc.py +1 -89
  11. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/spell.py +2 -42
  12. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/base.py +6 -1
  13. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/creature.py +2 -3
  14. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/item.py +1 -0
  15. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/npc.py +25 -1
  16. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/spell.py +0 -2
  17. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/schema.py +9 -69
  18. tibiawikisql-8.0.1/tibiawikisql/tasks/__init__.py +2 -0
  19. tibiawikisql-8.0.1/tibiawikisql/tasks/images.py +268 -0
  20. tibiawikisql-8.0.1/tibiawikisql/tasks/item_offers.py +107 -0
  21. tibiawikisql-8.0.1/tibiawikisql/tasks/item_proficiency_perks.py +96 -0
  22. tibiawikisql-8.0.1/tibiawikisql/tasks/loot_statistics.py +73 -0
  23. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/utils.py +83 -0
  24. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1/tibiawikisql.egg-info}/PKG-INFO +3 -3
  25. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql.egg-info/SOURCES.txt +6 -1
  26. tibiawikisql-7.0.3/tests/test_generation.py +0 -124
  27. tibiawikisql-7.0.3/tibiawikisql/__main__.py +0 -34
  28. tibiawikisql-7.0.3/tibiawikisql/generation.py +0 -684
  29. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/LICENSE +0 -0
  30. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/MANIFEST.in +0 -0
  31. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/README.md +0 -0
  32. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/requirements-docs.txt +0 -0
  33. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/requirements-server.txt +0 -0
  34. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/requirements-testing.txt +0 -0
  35. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/requirements.txt +0 -0
  36. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/setup.cfg +0 -0
  37. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tests/tests_models.py +0 -0
  38. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tests/tests_parsers.py +0 -0
  39. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tests/tests_schema.py +0 -0
  40. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tests/tests_wikiapi.py +0 -0
  41. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/api.py +0 -0
  42. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/database.py +0 -0
  43. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/errors.py +0 -0
  44. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/achievement.py +0 -0
  45. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/base.py +0 -0
  46. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/charm.py +0 -0
  47. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/creature.py +0 -0
  48. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/house.py +0 -0
  49. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/imbuement.py +0 -0
  50. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/mount.py +0 -0
  51. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/outfit.py +0 -0
  52. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/quest.py +0 -0
  53. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/update.py +0 -0
  54. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/models/world.py +0 -0
  55. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/__init__.py +0 -0
  56. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/achievement.py +0 -0
  57. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/book.py +0 -0
  58. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/charm.py +0 -0
  59. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/house.py +0 -0
  60. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/imbuement.py +0 -0
  61. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/key.py +0 -0
  62. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/mount.py +0 -0
  63. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/outfit.py +0 -0
  64. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/quest.py +0 -0
  65. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/update.py +0 -0
  66. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/parsers/world.py +0 -0
  67. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql/server.py +0 -0
  68. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql.egg-info/dependency_links.txt +0 -0
  69. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql.egg-info/entry_points.txt +0 -0
  70. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql.egg-info/requires.txt +0 -0
  71. {tibiawikisql-7.0.3 → tibiawikisql-8.0.1}/tibiawikisql.egg-info/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tibiawikisql
3
- Version: 7.0.3
3
+ Version: 8.0.1
4
4
  Summary: Python script that generates a SQLite database from TibiaWiki articles
5
- Author-email: Allan Galarza <allan.galarza@gmail.com>
6
- Maintainer-email: Allan Galarza <allan.galarza@gmail.com>
5
+ Author-email: Allan Galarza <contact@galarzaa.com>
6
+ Maintainer-email: Allan Galarza <contact@galarzaa.com>
7
7
  License: Apache 2.0
8
8
  Project-URL: Homepage, https://tibiawiki-sql.readthedocs.io/
9
9
  Project-URL: Documentation, https://tibiawiki-sql.readthedocs.io/
@@ -12,10 +12,10 @@ include = [
12
12
  name = "tibiawikisql"
13
13
  dynamic = ["version", "dependencies", "optional-dependencies"]
14
14
  authors = [
15
- { name = "Allan Galarza", email = "allan.galarza@gmail.com" }
15
+ { name = "Allan Galarza", email = "contact@galarzaa.com" }
16
16
  ]
17
17
  maintainers = [
18
- { name = "Allan Galarza", email = "allan.galarza@gmail.com" }
18
+ { name = "Allan Galarza", email = "contact@galarzaa.com" }
19
19
  ]
20
20
  license = { text = 'Apache 2.0' }
21
21
  description = "Python script that generates a SQLite database from TibiaWiki articles"
@@ -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)
@@ -1,8 +1,11 @@
1
1
  import unittest
2
2
 
3
+ import datetime
4
+
3
5
  from tests import load_resource
4
6
  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)
7
+ parse_date, parse_loot_statistics, parse_min_max, parse_sounds,
8
+ parse_weapon_proficiency_name, parse_weapon_proficiency_tables)
6
9
 
7
10
 
8
11
  class TestUtils(unittest.TestCase):
@@ -54,6 +57,9 @@ class TestUtils(unittest.TestCase):
54
57
  self.assertEqual(parse_integer("10056"), 10056)
55
58
  self.assertEqual(parse_integer("--"), 0)
56
59
 
60
+ def test_parse_date_ignores_comma_separated_time(self):
61
+ self.assertEqual(datetime.date(2026, 1, 27), parse_date("January 27, 2026, 16:00"))
62
+
57
63
  def test_parse_min_max(self):
58
64
  self.assertEqual(parse_min_max("5-20"), (5, 20))
59
65
  self.assertEqual(parse_min_max("50"), (0, 50))
@@ -81,3 +87,55 @@ class TestUtils(unittest.TestCase):
81
87
  self.assertEqual(client_color_to_rgb(3), 0x99)
82
88
  self.assertEqual(client_color_to_rgb(215), 0xffffff)
83
89
  self.assertEqual(client_color_to_rgb(216), 0)
90
+
91
+ def test_parse_weapon_proficiency_name(self):
92
+ content = load_resource("content_weapon_proficiency_name.txt")
93
+ mappings = parse_weapon_proficiency_name(content)
94
+
95
+ self.assertEqual(2, len(mappings))
96
+ self.assertEqual("Sanguine 1H Axe", mappings["Amber Axe"])
97
+ self.assertEqual("Sanguine 1H Club", mappings["Amber Cudgel"])
98
+ self.assertNotIn("#default", mappings)
99
+
100
+ def test_parse_weapon_proficiency_name_without_switch(self):
101
+ mappings = parse_weapon_proficiency_name("== Not a switch ==")
102
+ self.assertEqual({}, mappings)
103
+
104
+ def test_parse_weapon_proficiency_tables(self):
105
+ content = load_resource("content_weapon_proficiency_tables.txt")
106
+ tables = parse_weapon_proficiency_tables(content)
107
+
108
+ self.assertIn("Sanguine 1H Axe", tables)
109
+ self.assertIn("Sanguine 1H Club", tables)
110
+ self.assertIn(1, tables["Sanguine 1H Axe"])
111
+ self.assertIn(2, tables["Sanguine 1H Axe"])
112
+
113
+ perks = tables["Sanguine 1H Axe"][2]
114
+ self.assertEqual(3, len(perks))
115
+ self.assertIsNone(perks[0]["icon"])
116
+ self.assertEqual("Special Icon", perks[1]["icon"])
117
+
118
+ def test_parse_weapon_proficiency_tables_effect_fallback(self):
119
+ content = """===Sanguine 1H Axe===
120
+ {{Weapon Proficiency Table
121
+ |perk_1 =
122
+ {{Weapon Proficiency Button |skill_image=Axe Skill Bonus|icon=|effect=Fallback Effect}}
123
+ |perk_x =
124
+ {{Weapon Proficiency Button |skill_image=Ignored|icon=|text=Ignored}}
125
+ }}"""
126
+ tables = parse_weapon_proficiency_tables(content)
127
+ self.assertIn("Sanguine 1H Axe", tables)
128
+ self.assertIn(1, tables["Sanguine 1H Axe"])
129
+ self.assertEqual([1], list(tables["Sanguine 1H Axe"].keys()))
130
+ self.assertEqual("Fallback Effect", tables["Sanguine 1H Axe"][1][0]["effect"])
131
+
132
+ def test_parse_weapon_proficiency_tables_level_two_headings(self):
133
+ content = """==Sanguine 1H Axe==
134
+ {{Weapon Proficiency Table
135
+ |perk_1 =
136
+ {{Weapon Proficiency Button |skill_image=Axe Skill Bonus|icon=|text=+1 Axe Fighting}}
137
+ }}"""
138
+ tables = parse_weapon_proficiency_tables(content)
139
+ self.assertIn("Sanguine 1H Axe", tables)
140
+ self.assertIn(1, tables["Sanguine 1H Axe"])
141
+ 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.3"
7
+ __version__ = "8.0.1"
@@ -0,0 +1,62 @@
1
+ """Command line interface for tibiawiki-sql."""
2
+
3
+ import sqlite3
4
+
5
+ import click
6
+ import colorama
7
+
8
+ from tibiawikisql import __version__, generation
9
+ from tibiawikisql.utils import timed
10
+
11
+ DATABASE_FILE = "tibiawiki.db"
12
+ PARSING_ERRORS_FILE = "parsing-errors.log"
13
+
14
+ colorama.init()
15
+
16
+
17
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
18
+ @click.version_option(__version__, "-V", "--version")
19
+ def cli() -> None:
20
+ """Run tibiawiki-sql commands."""
21
+
22
+
23
+ @cli.command(name="generate")
24
+ @click.option("-i", "--skip-images", help="Skip fetching and loading images to the database.", is_flag=True)
25
+ @click.option("-o", "--db-name", help="Name for the database file.", default=DATABASE_FILE)
26
+ @click.option("-d", "--skip-deprecated", help="Skips fetching deprecated articles and their images.", is_flag=True)
27
+ @click.option(
28
+ "--log-parsing-errors",
29
+ help=f"Write every parsing error to {PARSING_ERRORS_FILE}.",
30
+ is_flag=True,
31
+ )
32
+ @click.option(
33
+ "-c",
34
+ "--skip-category",
35
+ "skip_categories",
36
+ multiple=True,
37
+ type=click.Choice(sorted(generation.CATEGORIES), case_sensitive=False),
38
+ help=(
39
+ "Skip specific categories. Can be repeated."
40
+ ),
41
+ )
42
+ def generate(
43
+ skip_images: bool,
44
+ db_name: str,
45
+ skip_deprecated: bool,
46
+ log_parsing_errors: bool,
47
+ skip_categories: tuple[str, ...],
48
+ ) -> None:
49
+ """Generates a database file."""
50
+ with timed() as t, sqlite3.connect(db_name) as conn:
51
+ generation.generate(
52
+ conn,
53
+ skip_images=skip_images,
54
+ skip_deprecated=skip_deprecated,
55
+ skip_categories=skip_categories,
56
+ parsing_errors_file=PARSING_ERRORS_FILE if log_parsing_errors else None,
57
+ )
58
+ click.echo(f"Command finished in {t.elapsed:.2f} seconds.")
59
+
60
+
61
+ if __name__ == "__main__":
62
+ cli()