Skip to content

Commit b4a8060

Browse files
committed
reorganize post-generate tasks
1 parent 687856f commit b4a8060

9 files changed

Lines changed: 886 additions & 553 deletions

File tree

docs/intro.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ This fetches all the relevant articles from TibiaWiki and stores them in the dat
3434

3535
It accepts the following parameters:
3636

37-
- `-s`/`--skip-images` Option to skip fetching and saving images.
38-
- `-db`/ `--db-name` The name of the generated database file. `tibiawiki.db` by default.
39-
- `-sd`/ `--skip-deprecated` Option to skip deprecated articles when parsing.
37+
- `-i`/`--skip-images` Option to skip fetching and saving images.
38+
- `-o`/ `--db-name` The name of the generated database file. `tibiawiki.db` by default.
39+
- `-d`/ `--skip-deprecated` Option to skip deprecated articles when parsing.
40+
- `-c`/ `--skip-category` Option to skip one or more categories (repeatable), using internal category keys such as `achievements`, `items`, `creatures`, `houses`, or `charms`.
41+
42+
If skipping a category would break a hard dependency for another category, the dependent category is skipped automatically and a warning is shown.
4043

4144
The generated database is saved in the current directory, as well as a folder called `images` with all the fetched images.
4245

tests/test_generation.py

Lines changed: 164 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import datetime
22
import sqlite3
33
import unittest
4-
from unittest.mock import patch
4+
from unittest.mock import Mock, patch
5+
6+
from click.testing import CliRunner
57

68
from tests import load_resource
9+
from tibiawikisql import __main__ as cli_module
10+
from tibiawikisql import generation as generation_module
711
from tibiawikisql.api import Article
8-
from tibiawikisql.generation import (
9-
WEAPON_PROFICIENCY_NAME_ARTICLE,
10-
WEAPON_PROFICIENCY_TABLES_ARTICLE,
11-
generate_item_proficiency_perks,
12-
wiki_client,
13-
)
12+
from tibiawikisql.generation import WEAPON_PROFICIENCY_NAME_ARTICLE, WEAPON_PROFICIENCY_TABLES_ARTICLE
1413
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
1516

1617

1718
class TestGeneration(unittest.TestCase):
@@ -46,11 +47,18 @@ def test_generate_item_proficiency_perks(self):
4647
)
4748
data_store = {"items_map": {"amber axe": 1, "amber cudgel": 2}}
4849

49-
with (
50-
patch.object(wiki_client, "get_article", side_effect=[mapping_article, tables_article]),
51-
patch("tibiawikisql.generation.click.echo") as mock_echo,
52-
):
53-
generate_item_proficiency_perks(self.conn, data_store)
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+
)
5462

5563
rows = self.conn.execute(
5664
"SELECT item_id, proficiency_level, skill_image, icon, effect "
@@ -91,11 +99,18 @@ def test_generate_item_proficiency_perks_warn_and_continue(self):
9199
)
92100
data_store = {"items_map": {"amber axe": 1, "amber cudgel": 2}}
93101

94-
with (
95-
patch.object(wiki_client, "get_article", side_effect=[mapping_article, tables_article]),
96-
patch("tibiawikisql.generation.click.echo") as mock_echo,
97-
):
98-
generate_item_proficiency_perks(self.conn, data_store)
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+
)
99114

100115
rows = self.conn.execute(
101116
"SELECT item_id, proficiency_level, skill_image, icon, effect FROM item_proficiency_perk",
@@ -118,20 +133,31 @@ def get_article_side_effect(title: str) -> Article | None:
118133
return mapping_article
119134
return None
120135

121-
with (
122-
patch.object(wiki_client, "get_article", side_effect=get_article_side_effect) as mock_get_article,
123-
patch("tibiawikisql.generation.click.echo") as mock_echo,
124-
):
125-
generate_item_proficiency_perks(self.conn, data_store)
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+
)
126148

127149
rows = self.conn.execute("SELECT COUNT(*) FROM item_proficiency_perk").fetchone()
128150
self.assertEqual(0, rows[0])
129151
self.assertEqual(
130-
[WEAPON_PROFICIENCY_NAME_ARTICLE, WEAPON_PROFICIENCY_TABLES_ARTICLE],
131-
[call.args[0] for call in mock_get_article.call_args_list],
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],
132157
)
133158
messages = " ".join(call.args[0] for call in mock_echo.call_args_list if call.args)
134159
self.assertIn("Could not fetch weapon proficiency pages", messages)
160+
self.assertIn(WEAPON_PROFICIENCY_TABLES_ARTICLE, messages)
135161

136162
def test_generate_item_proficiency_perks_case_insensitive_section_match(self):
137163
timestamp = datetime.datetime.fromisoformat("2024-01-01T00:00:00+00:00")
@@ -154,13 +180,122 @@ def test_generate_item_proficiency_perks_case_insensitive_section_match(self):
154180
)
155181
data_store = {"items_map": {"stale bread of ancientness": 3}}
156182

157-
with (
158-
patch.object(wiki_client, "get_article", side_effect=[mapping_article, tables_article]),
159-
patch("tibiawikisql.generation.click.echo"),
160-
):
161-
generate_item_proficiency_perks(self.conn, data_store)
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+
)
162194

163195
rows = self.conn.execute(
164196
"SELECT item_id, proficiency_level, skill_image, icon, effect FROM item_proficiency_perk",
165197
).fetchall()
166198
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)

tibiawikisql/__main__.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,29 @@ def cli():
1919

2020

2121
@cli.command(name="generate")
22-
@click.option('-s', '--skip-images', help="Skip fetching and loading images to the database.", is_flag=True)
23-
@click.option('-db', '--db-name', help="Name for the database file.", default=DATABASE_FILE)
24-
@click.option('-sd', '--skip-deprecated', help="Skips fetching deprecated articles and their images.", is_flag=True)
25-
def generate(skip_images, db_name, skip_deprecated):
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):
2636
"""Generates a database file."""
2737
with timed() as t:
2838
with sqlite3.connect(db_name) as conn:
29-
generation.generate(conn, skip_images, skip_deprecated)
39+
generation.generate(
40+
conn,
41+
skip_images=skip_images,
42+
skip_deprecated=skip_deprecated,
43+
skip_categories=skip_categories,
44+
)
3045
click.echo(f"Command finished in {t.elapsed:.2f} seconds.")
3146

3247

0 commit comments

Comments
 (0)