11"""Models related to the Tibia.com character page."""
22from __future__ import annotations
33
4+ import logging
45import re
56from collections import OrderedDict
6- from typing import List , Optional , TYPE_CHECKING
7+ from typing import Callable , Dict , List , Optional , TYPE_CHECKING
78
89from tibiapy .builders import CharacterBuilder
910from tibiapy .enums import Sex , Vocation
4041 "CharacterParser" ,
4142)
4243
44+ logger = logging .getLogger (__name__ )
45+
4346
4447class CharacterParser :
4548 """A parser for characters from Tibia.com."""
@@ -70,32 +73,31 @@ def from_content(cls, content: str) -> Optional[Character]:
7073 if messsage_table and "Could not find character" in messsage_table .text :
7174 return None
7275
73- if "Character Information" in tables :
74- cls ._parse_character_information (builder , tables ["Character Information" ])
75- else :
76+ table_parsers = {
77+ "Character Information" : lambda t : cls ._parse_character_information (builder , t ),
78+ "Account Badges" : lambda t : cls ._parse_account_badges (builder , t ),
79+ "Account Achievements" : lambda t : cls ._parse_achievements (builder , t ),
80+ "Account Information" : lambda t : cls ._parse_account_information (builder , t ),
81+ "Character Deaths" : lambda t : cls ._parse_deaths (builder , t ),
82+ "Characters" : lambda t : cls ._parse_other_characters (builder , t ),
83+ }
84+
85+ if "Character Information" not in tables :
7686 raise InvalidContent ("content does not contain a tibia.com character information page." )
7787
78- builder .achievements (cls ._parse_achievements (tables .get ("Account Achievements" , [])))
79- if "Account Badges" in tables :
80- builder .account_badges (cls ._parse_badges (tables ["Account Badges" ]))
88+ for title , table in tables .items ():
89+ if title in table_parsers :
90+ action = table_parsers [title ]
91+ action (table )
8192
82- cls ._parse_deaths (builder , tables .get ("Character Deaths" , []))
83- builder .account_information (cls ._parse_account_information (tables .get ("Account Information" , [])))
84- builder .other_characters (cls ._parse_other_characters (tables .get ("Characters" , [])))
8593 return builder .build ()
8694
8795 @classmethod
88- def _parse_account_information (cls , rows ):
89- """Parse the character's account information.
90-
91- Parameters
92- ----------
93- rows: :class:`list` of :class:`bs4.Tag`, optional
94- A list of all rows contained in the table.
95- """
96+ def _parse_account_information (cls , builder : CharacterBuilder , rows : list [bs4 .Tag ]):
97+ """Parse the character's account information."""
9698 acc_info = {}
9799 if not rows :
98- return None
100+ return
99101
100102 for row in rows :
101103 cols_raw = row .select ("td" )
@@ -108,18 +110,11 @@ def _parse_account_information(cls, rows):
108110 created = parse_tibia_datetime (acc_info ["created" ])
109111 loyalty_title = None if acc_info ["loyalty_title" ] == "(no title)" else acc_info ["loyalty_title" ]
110112 position = acc_info .get ("position" )
111- return AccountInformation (created = created , loyalty_title = loyalty_title , position = position )
113+ builder . account_information ( AccountInformation (created = created , loyalty_title = loyalty_title , position = position ) )
112114
113115 @classmethod
114- def _parse_achievements (cls , rows ):
115- """Parse the character's displayed achievements.
116-
117- Parameters
118- ----------
119- rows: :class:`list` of :class:`bs4.Tag`
120- A list of all rows contained in the table.
121- """
122- achievements = []
116+ def _parse_achievements (cls , builder : CharacterBuilder , rows : List [bs4 .Tag ]):
117+ """Parse the character's displayed achievements."""
123118 for row in rows :
124119 cols = row .select ("td" )
125120 if len (cols ) != 2 :
@@ -129,94 +124,62 @@ def _parse_achievements(cls, rows):
129124 grade = str (field ).count ("achievement-grade-symbol" )
130125 name = value .text .strip ()
131126 secret_image = value .select_one ("img" )
132- secret = False
133- if secret_image :
134- secret = True
135-
136- achievements .append (Achievement (name = name , grade = grade , is_secret = secret ))
127+ secret = secret_image is not None
137128
138- return achievements
129+ builder . add_achievement ( Achievement ( name = name , grade = grade , is_secret = secret ))
139130
140131 @classmethod
141- def _parse_badges (cls , rows : List [bs4 .Tag ]):
142- """Parse the character's displayed badges.
143-
144- Parameters
145- ----------
146- rows: :class:`list` of :class:`bs4.Tag`
147- A list of all rows contained in the table.
148- """
132+ def _parse_account_badges (cls , builder : CharacterBuilder , rows : List [bs4 .Tag ]):
133+ """Parse the character's displayed badges."""
149134 row = rows [0 ]
150135 columns = row .select ("td > span" )
151- account_badges = []
152136 for column in columns :
153137 popup_span = column .select_one ("span.HelperDivIndicator" )
154138 if not popup_span :
155139 # Badges are visible, but none selected.
156- return []
140+ return
157141
158142 popup = parse_popup (popup_span ["onmouseover" ])
159143 name = popup [0 ]
160144 description = popup [1 ].text
161145 icon_image = column .select_one ("img" )
162146 icon_url = icon_image ["src" ]
163- account_badges .append (AccountBadge (name = name , icon_url = icon_url , description = description ))
164-
165- return account_badges
147+ builder .add_account_badge (AccountBadge (name = name , icon_url = icon_url , description = description ))
166148
167149 @classmethod
168- def _parse_character_information (cls , builder : CharacterBuilder , rows ):
169- """
170- Parse the character's basic information and applies the found values.
150+ def _parse_character_information (cls , builder : CharacterBuilder , rows : List [bs4 .Tag ]):
151+ """Parse the character's basic information and applies the found values."""
152+ field_actions : dict [str , Callable [[bs4 .Tag , str ], None ]] = {
153+ "name" : lambda rv , v : cls ._parse_name_field (builder , v ),
154+ "title" : lambda rv , v : cls ._parse_titles (builder , v ),
155+ "former names" : lambda rv , v : builder .former_names ([fn .strip () for fn in v .split ("," )]),
156+ "former world" : lambda rv , v : builder .former_world (v ),
157+ "sex" : lambda rv , v : builder .sex (try_enum (Sex , v )),
158+ "vocation" : lambda rv , v : builder .vocation (try_enum (Vocation , v )),
159+ "level" : lambda rv , v : builder .level (parse_integer (v )),
160+ "achievement points" : lambda rv , v : builder .achievement_points (parse_integer (v )),
161+ "world" : lambda rv , v : builder .world (v ),
162+ "residence" : lambda rv , v : builder .residence (v ),
163+ "last login" : lambda rv , v : builder .last_login (None ) if "never logged" in v .lower () else builder .last_login (
164+ parse_tibia_datetime (v ),
165+ ),
166+ "position" : lambda rv , v : builder .position (v ),
167+ "comment" : lambda rv , v : builder .comment (v ),
168+ "account status" : lambda rv , v : builder .is_premium ("premium" in v .lower ()),
169+ "married to" : lambda rv , v : builder .married_to (v ),
170+ "house" : lambda rv , v : cls ._parse_house_column (builder , rv ),
171+ "guild membership" : lambda rv , v : cls ._parse_guild_column (builder , rv ),
172+ }
171173
172- Parameters
173- ----------
174- rows: :class:`list` of :class:`bs4.Tag`
175- A list of all rows contained in the table.
176- """
177174 for row in rows :
178- cols_raw = row .select ("td" )
179- cols = [clean_text (ele ) for ele in cols_raw ]
180- field , value = cols
175+ raw_field , raw_value = row .select ("td" )
176+ field , value = clean_text (raw_field ), clean_text (raw_value )
181177 field = field .replace (":" , "" ).lower ()
182- if field == "name" :
183- cls ._parse_name_field (builder , value )
184- elif field == "title" :
185- cls ._parse_titles (builder , value )
186- elif field == "former names" :
187- builder .former_names ([fn .strip () for fn in value .split ("," )])
188- elif field == "former world" :
189- builder .former_world (value )
190- elif field == "sex" :
191- builder .sex (try_enum (Sex , value ))
192- elif field == "vocation" :
193- builder .vocation (try_enum (Vocation , value ))
194- elif field == "level" :
195- builder .level (parse_integer (value ))
196- elif field == "achievement points" :
197- builder .achievement_points (parse_integer (value ))
198- elif field == "world" :
199- builder .world (value )
200- elif field == "residence" :
201- builder .residence (value )
202- elif field == "last login" :
203- if "never logged" in value .lower ():
204- builder .last_login (None )
205- else :
206- builder .last_login (parse_tibia_datetime (value ))
207-
208- elif field == "position" :
209- builder .position (value )
210- elif field == "comment" :
211- builder .comment (value )
212- elif field == "account status" :
213- builder .is_premium ("premium" in value .lower ())
214- elif field == "married to" :
215- builder .married_to (value )
216- elif field == "house" :
217- cls ._parse_house_column (builder , cols_raw [1 ])
218- elif field == "guild membership" :
219- cls ._parse_guild_column (builder , cols_raw [1 ])
178+ if field in field_actions :
179+ action = field_actions [field ]
180+ action (raw_value , value )
181+ else :
182+ logger .debug (f"Unhandled character information field found: { field } " )
220183
221184 @classmethod
222185 def _parse_name_field (cls , builder : CharacterBuilder , value : str ):
@@ -268,14 +231,8 @@ def _parse_guild_column(cls, builder: CharacterBuilder, column: bs4.Tag):
268231 builder .guild_membership (GuildMembership (name = clean_text (guild_link ), rank = rank .strip ()))
269232
270233 @classmethod
271- def _parse_deaths (cls , builder : CharacterBuilder , rows ):
272- """Parse the character's recent deaths.
273-
274- Parameters
275- ----------
276- rows: :class:`list` of :class:`bs4.Tag`
277- A list of all rows contained in the table.
278- """
234+ def _parse_deaths (cls , builder : CharacterBuilder , rows : List [bs4 .Tag ]):
235+ """Parse the character's recent deaths."""
279236 for row in rows :
280237 cols = row .select ("td" )
281238 if len (cols ) != 2 :
@@ -299,8 +256,8 @@ def _parse_deaths(cls, builder: CharacterBuilder, rows):
299256 assists_name_list = link_search .findall (assists_desc )
300257
301258 killers_name_list = split_list (killers_desc )
302- killers_list = [cls ._parse_killer (k ) for k in killers_name_list ]
303- assists_list = [cls ._parse_killer (k ) for k in assists_name_list ]
259+ killers_list = [cls ._parse_participant (k ) for k in killers_name_list ]
260+ assists_list = [cls ._parse_participant (k ) for k in assists_name_list ]
304261 builder .add_death (Death (
305262 level = level ,
306263 killers = killers_list ,
@@ -309,18 +266,8 @@ def _parse_deaths(cls, builder: CharacterBuilder, rows):
309266 ))
310267
311268 @classmethod
312- def _parse_killer (cls , killer ):
313- """Parse a killer into a dictionary.
314-
315- Parameters
316- ----------
317- killer: :class:`str`
318- The killer's raw HTML string.
319-
320- Returns
321- -------
322- :class:`dict`: A dictionary containing the killer's info.
323- """
269+ def _parse_participant (cls , killer : str ) -> DeathParticipant :
270+ """Parse a participant's information from their raw HTML string."""
324271 # If the killer contains a link, it is a player.
325272 name = clean_text (killer )
326273 player = False
@@ -344,15 +291,8 @@ def _parse_killer(cls, killer):
344291 return DeathParticipant (name = name , is_player = player , summon = summon , is_traded = traded )
345292
346293 @classmethod
347- def _parse_other_characters (cls , rows ):
348- """Parse the character's other visible characters.
349-
350- Parameters
351- ----------
352- rows: :class:`list` of :class:`bs4.Tag`
353- A list of all rows contained in the table.
354- """
355- other_characters = []
294+ def _parse_other_characters (cls , builder : CharacterBuilder , rows : List [bs4 .Tag ]):
295+ """Parse the character's other visible characters."""
356296 for row in rows [1 :]:
357297 cols_raw = row .select ("td" )
358298 cols = [ele .text .strip () for ele in cols_raw ]
@@ -376,32 +316,23 @@ def _parse_other_characters(cls, rows):
376316 if "CipSoft Member" in status :
377317 position = "CipSoft Member"
378318
379- other_characters .append (OtherCharacter (name = name , world = world , is_online = "online" in status ,
380- is_deleted = "deleted" in status , is_main = main , position = position ,
381- is_traded = traded ))
382-
383- return other_characters
319+ builder .add_other_character (OtherCharacter (
320+ name = name ,
321+ world = world ,
322+ is_online = "online" in status ,
323+ is_deleted = "deleted" in status ,
324+ is_main = main ,
325+ position = position ,
326+ is_traded = traded ,
327+ ))
384328
385329 @classmethod
386- def _parse_tables (cls , parsed_content ):
387- """
388- Parse the information tables contained in a character's page.
389-
390- Parameters
391- ----------
392- parsed_content: :class:`bs4.BeautifulSoup`
393- A :class:`BeautifulSoup` object containing all the content.
394-
395- Returns
396- -------
397- :class:`OrderedDict`[str, :class:`list`of :class:`bs4.Tag`]
398- A dictionary containing all the table rows, with the table headers as keys.
399- """
330+ def _parse_tables (cls , parsed_content : bs4 .BeautifulSoup ) -> Dict [str , List [bs4 .Tag ]]:
331+ """Parse the tables contained in a character's page and returns a mapping of their titles and rows."""
400332 tables = parsed_content .select ('table[width="100%"]' )
401333 output = OrderedDict ()
402334 for table in tables :
403- container = table .find_parent ("div" , {"class" : "TableContainer" })
404- if container :
335+ if container := table .find_parent ("div" , {"class" : "TableContainer" }):
405336 caption_container = container .select_one ("div.CaptionContainer" )
406337 title = caption_container .text .strip ()
407338 offset = 0
0 commit comments