Skip to content

Commit 8bd84ab

Browse files
committed
Added boostable bosses section
1 parent e0a8962 commit 8bd84ab

File tree

3 files changed

+307
-16
lines changed

3 files changed

+307
-16
lines changed

serve.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ async def get_boosted_creature(request: web.Request):
108108
return json_response(response)
109109

110110

111+
@routes.get('/bosses/boosted')
112+
async def get_boosted_bosses(request: web.Request):
113+
response = await app["tibiapy"].fetch_boosted_boss()
114+
return json_response(response)
115+
116+
117+
@routes.get('/boosted')
118+
async def get_boosted_creatures(request: web.Request):
119+
response = await app["tibiapy"].fetch_boosted_creature_and_boss()
120+
return json_response(response)
121+
122+
123+
@routes.get('/bosses')
124+
async def get_library_bosses(request: web.Request):
125+
response = await app["tibiapy"].fetch_library_bosses()
126+
return json_response(response)
127+
128+
111129
@routes.get('/creatures/{name}')
112130
async def get_library_creature(request: web.Request):
113131
name = request.match_info.get('name')

tibiapy/client.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import tibiapy
1313
from tibiapy import Auction, AuctionFilters, CharacterBazaar, Leaderboard, Spell, SpellsSection, abc
1414
from tibiapy.character import Character
15-
from tibiapy.creature import Creature, CreatureEntry, CreaturesSection
15+
from tibiapy.creature import BoostableBosses, BoostedCreatures, Creature, CreatureEntry, CreaturesSection
1616
from tibiapy.enums import BattlEyeHighscoresFilter, Category, HouseType, NewsCategory, \
1717
NewsType, VocationFilter
1818
from tibiapy.errors import Forbidden, NetworkError, SiteMaintenanceError
@@ -754,6 +754,96 @@ async def fetch_forum_announcement(self, announcement_id, *, test=False):
754754

755755
# endregion
756756

757+
async def fetch_boosted_creature_and_boss(self, *, test=False):
758+
"""Fetch today's boosted creature and boss.
759+
760+
.. versionadded:: 5.3.0
761+
762+
Parameters
763+
----------
764+
test: :class:`bool`
765+
Whether to request the test website instead.
766+
767+
Returns
768+
-------
769+
:class:`TibiaResponse` of :class:`BoostedCreatures`
770+
The boosted creature and boss of the day.
771+
772+
Raises
773+
------
774+
Forbidden
775+
If a 403 Forbidden error was returned.
776+
This usually means that Tibia.com is rate-limiting the client because of too many requests.
777+
NetworkError
778+
If there's any connection errors during the request.
779+
"""
780+
response = await self._request("GET", NewsArchive.get_url(), test=test)
781+
start_time = time.perf_counter()
782+
boosted_creatures = BoostedCreatures.from_header(response.content)
783+
parsing_time = time.perf_counter() - start_time
784+
return TibiaResponse(response, boosted_creatures, parsing_time)
785+
786+
# Region Bosses
787+
async def fetch_boosted_boss(self, *, test=False):
788+
"""Fetch today's boosted boss.
789+
790+
.. versionadded:: 5.3.0
791+
792+
Parameters
793+
----------
794+
test: :class:`bool`
795+
Whether to request the test website instead.
796+
797+
Returns
798+
-------
799+
:class:`TibiaResponse` of :class:`BossEntry`
800+
The boosted boss of the day.
801+
802+
Raises
803+
------
804+
Forbidden
805+
If a 403 Forbidden error was returned.
806+
This usually means that Tibia.com is rate-limiting the client because of too many requests.
807+
NetworkError
808+
If there's any connection errors during the request.
809+
"""
810+
response = await self._request("GET", NewsArchive.get_url(), test=test)
811+
start_time = time.perf_counter()
812+
boosted_creature = BoostableBosses.boosted_boss_from_header(response.content)
813+
parsing_time = time.perf_counter() - start_time
814+
return TibiaResponse(response, boosted_creature, parsing_time)
815+
816+
async def fetch_library_bosses(self, *, test=False):
817+
"""Fetch the bosses from the library section.
818+
819+
.. versionadded:: 4.0.0
820+
821+
Parameters
822+
----------
823+
test: :class:`bool`
824+
Whether to request the test website instead.
825+
826+
Returns
827+
-------
828+
:class:`TibiaResponse` of :class:`BoostableBosses`
829+
The creature's section in Tibia.com
830+
831+
Raises
832+
------
833+
Forbidden
834+
If a 403 Forbidden error was returned.
835+
This usually means that Tibia.com is rate-limiting the client because of too many requests.
836+
NetworkError
837+
If there's any connection errors during the request.
838+
"""
839+
response = await self._request("GET", BoostableBosses.get_url(), test=test)
840+
start_time = time.perf_counter()
841+
boosted_creature = BoostableBosses.from_content(response.content)
842+
parsing_time = time.perf_counter() - start_time
843+
return TibiaResponse(response, boosted_creature, parsing_time)
844+
845+
# endregion
846+
757847
# region Creatures
758848
async def fetch_boosted_creature(self, *, test=False):
759849
"""Fetch today's boosted creature.
@@ -1133,9 +1223,9 @@ async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE, stat
11331223
house_type: :class:`HouseType`
11341224
The type of building. House by default.
11351225
status: :class:`HouseStatus`, optional
1136-
The house status to filter results. By default no filters will be applied.
1226+
The house status to filter results. By default, no filters will be applied.
11371227
order: :class:`HouseOrder`, optional
1138-
The ordering to use for the results. By default they are sorted by name.
1228+
The ordering to use for the results. By default, they are sorted by name.
11391229
test: :class:`bool`
11401230
Whether to request the test website instead.
11411231

tibiapy/creature.py

Lines changed: 196 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Models related to the creatures section in the library."""
2+
import os
23
import re
34
import urllib.parse
45
from typing import List, Optional
@@ -9,14 +10,18 @@
910
from tibiapy.errors import InvalidContent
1011

1112
__all__ = (
13+
"BoostedCreatures",
14+
"BoostableBosses",
15+
"BossEntry",
1216
"CreatureEntry",
1317
"CreaturesSection",
1418
"Creature",
1519
)
1620

1721
from tibiapy.utils import parse_tibiacom_content, get_tibia_url
1822

19-
BOOSTED_ALT = "Today's boosted creature: "
23+
BOOSTED_ALT = re.compile("Today's boosted \w+: ")
24+
2025

2126
HP_PATTERN = re.compile(r"have (\d+) hitpoints")
2227
EXP_PATTERN = re.compile(r"yield (\d+) experience")
@@ -27,6 +32,193 @@
2732
MANA_COST = re.compile(r"takes (\d+) mana")
2833

2934

35+
class BoostedCreatures(abc.Serializable):
36+
"""Contains both boosted creature and boosted boss.
37+
38+
Attributes
39+
----------
40+
creature: :class:`CreatureEntry`
41+
The boosted creature of the day.
42+
boss: :class:`BossEntry`
43+
The boosted boss of the day.
44+
"""
45+
46+
__slots__ = (
47+
"creature",
48+
"boss",
49+
)
50+
51+
def __init__(self, creature, boss):
52+
self.creature: CreatureEntry = creature
53+
self.boss: BossEntry = boss
54+
55+
@classmethod
56+
def _parse_boosted_platform(cls, parsed_content: bs4.BeautifulSoup, tag_id: str):
57+
img = parsed_content.find("img", attrs={"id": tag_id})
58+
name = BOOSTED_ALT.sub("", img["title"]).strip()
59+
image_url = img["src"]
60+
identifier = image_url.split("/")[-1].replace(".gif", "")
61+
return name, identifier
62+
63+
@classmethod
64+
def from_header(cls, content: str):
65+
"""Parses both boosted creature and boss from the content of any section in Tibia.com
66+
67+
Parameters
68+
----------
69+
content: :class:`str`
70+
The HTML content of the page.
71+
72+
Returns
73+
-------
74+
:class:`BoostedCreatures`
75+
The boosted creature an boss.
76+
77+
Raises
78+
------
79+
InvalidContent
80+
If content is not the HTML of a Tibia.com page.
81+
"""
82+
try:
83+
parsed_content = bs4.BeautifulSoup(content.replace('ISO-8859-1', 'utf-8'), "lxml",
84+
parse_only=bs4.SoupStrainer("div", attrs={"id": "RightArtwork"}))
85+
creature_name, creature_identifier = cls._parse_boosted_platform(parsed_content, "Monster")
86+
boss_name, boss_identifier = cls._parse_boosted_platform(parsed_content, "Boss")
87+
return cls(CreatureEntry(creature_name, creature_identifier), BossEntry(boss_name, boss_identifier))
88+
except (TypeError, NameError, KeyError) as e:
89+
raise InvalidContent("content is not from Tibia.com", e)
90+
91+
92+
class BoostableBosses(abc.Serializable):
93+
"""Represents the boostable bosses section in the Tibia.com library
94+
95+
Attributes
96+
----------
97+
boosted_boss: :class:`BossEntry`
98+
The current boosted boss.
99+
bosses: list of :class:`BossEntry`
100+
The list of boostable bosses.
101+
"""
102+
103+
__slots__ = (
104+
"boosted_boss",
105+
"bosses",
106+
)
107+
108+
def __init__(self, boosted_boss, bosses):
109+
self.boosted_boss: BossEntry = boosted_boss
110+
self.bosses: List[BossEntry] = bosses
111+
112+
@classmethod
113+
def get_url(cls):
114+
"""Get the URL to the Tibia.com boostable bosses.
115+
116+
Returns
117+
-------
118+
:class:`str`:
119+
The URL to the Tibia.com library section.
120+
"""
121+
return get_tibia_url("library", "boostablebosses")
122+
123+
@classmethod
124+
def from_content(cls, content):
125+
"""Create an instance of the class from the html content of the boostable bosses library's page.
126+
127+
Parameters
128+
----------
129+
content: :class:`str`
130+
The HTML content of the page.
131+
132+
Returns
133+
-------
134+
:class:`BoostableBosses`
135+
The Boostable Bosses section.
136+
137+
Raises
138+
------
139+
InvalidContent
140+
If content is not the HTML of a creature library's page.
141+
"""
142+
try:
143+
parsed_content = parse_tibiacom_content(content)
144+
boosted_creature_table = parsed_content.find("div", {"class": "TableContainer"})
145+
boosted_creature_text = boosted_creature_table.find("div", {"class": "Text"})
146+
if not boosted_creature_text or "Boosted" not in boosted_creature_text.text:
147+
return None
148+
boosted_boss_tag = boosted_creature_table.find("b")
149+
boosted_boss_image = boosted_creature_table.find("img")
150+
image_url = urllib.parse.urlparse(boosted_boss_image["src"])
151+
boosted_boss = BossEntry(boosted_boss_tag.text, os.path.basename(image_url.path).replace(".gif", ""))
152+
153+
list_table = parsed_content.find("div", style=lambda v: v and 'display: table' in v)
154+
entries_container = list_table.find_all("div", style=lambda v: v and 'float: left' in v)
155+
entries = []
156+
for entry_container in entries_container:
157+
name = entry_container.text.strip()
158+
image = entry_container.find("img")
159+
image_url = urllib.parse.urlparse(image["src"])
160+
identifier = os.path.basename(image_url.path).replace(".gif", "")
161+
entries.append(BossEntry(name, identifier))
162+
return cls(boosted_boss, entries)
163+
except (AttributeError, ValueError) as e:
164+
raise InvalidContent("content is not the boosted boss's library", e)
165+
166+
167+
@classmethod
168+
def boosted_boss_from_header(cls, content):
169+
"""Get the boosted boss from any Tibia.com page.
170+
171+
Parameters
172+
----------
173+
content: :class:`str`
174+
The HTML content of a Tibia.com page.
175+
176+
Returns
177+
-------
178+
:class:`BossEntry`
179+
The boosted boss of the day.
180+
181+
Raises
182+
------
183+
InvalidContent
184+
If content is not the HTML of a Tibia.com's page.
185+
"""
186+
return BoostedCreatures.from_header(content).boss
187+
188+
189+
class BossEntry(abc.Serializable):
190+
"""Represents a boss in the boostable bosses section in the Tibia.com library.
191+
192+
Attributes
193+
----------
194+
name: :class:`str`
195+
The name of the boss..
196+
identifier: :class:`str`
197+
The internal name of the boss. Used for images.
198+
"""
199+
200+
__slots__ = (
201+
"name",
202+
"identifier",
203+
)
204+
205+
_serializable_properties = (
206+
"image_url",
207+
)
208+
209+
def __init__(self, name, identifier=None):
210+
self.name: str = name
211+
self.identifier: str = identifier
212+
213+
def __repr__(self):
214+
return f"<{self.__class__.__name__} name={self.name!r} identifier={self.identifier!r}>"
215+
216+
@property
217+
def image_url(self):
218+
""":class:`str`: The URL to this boss's image."""
219+
return f"https://static.tibia.com/images/library/{self.identifier}.gif"
220+
221+
30222
class CreaturesSection(abc.Serializable):
31223
"""Represents the creature's section in the Tibia.com library.
32224
@@ -77,16 +269,7 @@ def boosted_creature_from_header(cls, content):
77269
InvalidContent
78270
If content is not the HTML of a Tibia.com's page.
79271
"""
80-
try:
81-
parsed_content = bs4.BeautifulSoup(content.replace('ISO-8859-1', 'utf-8'), "lxml",
82-
parse_only=bs4.SoupStrainer("div", attrs={"id": "RightArtwork"}))
83-
img = parsed_content.find("img", attrs={"id": "Monster"})
84-
name = img["title"].replace(BOOSTED_ALT, "").strip()
85-
image_url = img["src"]
86-
identifier = image_url.split("/")[-1].replace(".gif", "")
87-
return CreatureEntry(name, identifier)
88-
except TypeError as e:
89-
raise InvalidContent("content is not from Tibia.com", e)
272+
return BoostedCreatures.from_header(content).creature
90273

91274
@classmethod
92275
def from_content(cls, content):
@@ -99,8 +282,8 @@ def from_content(cls, content):
99282
100283
Returns
101284
-------
102-
:class:`Character`
103-
The character contained in the page.
285+
:class:`CreaturesSection`
286+
The creatures section from Tibia.com.
104287
105288
Raises
106289
------

0 commit comments

Comments
 (0)