diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b95bd0e4..a2f4d71d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,14 @@ Changelog Due to this library relying on external content, older versions are not guaranteed to work. Try to always use the latest version. +.. _v2.1.0: + +2.1.0 (2019-06-17) +================== + +- Added ways to sort and filter House list results like in Tibia.com. +- Added support to get the Boosted Creature of the day. + .. _v2.0.1: 2.0.1 (2019-06-04) diff --git a/docs/api.rst b/docs/api.rst index 71e1ddbe..257439dc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -18,9 +18,6 @@ Enumerations ============ Enumerations are provided for various values in order to avoid depending on strings. -.. autoclass:: HouseType - :members: - :undoc-members: .. autoclass:: AccountStatus :members: @@ -30,10 +27,18 @@ Enumerations are provided for various values in order to avoid depending on stri :members: :undoc-members: +.. autoclass:: HouseOrder + :members: + :undoc-members: + .. autoclass:: HouseStatus :members: :undoc-members: +.. autoclass:: HouseType + :members: + :undoc-members: + .. autoclass:: NewsCategory :members: :undoc-members: @@ -50,6 +55,11 @@ Enumerations are provided for various values in order to avoid depending on stri :members: :undoc-members: +.. autoclass:: TournamentWorldType + :members: + :undoc-members: + + .. autoclass:: TransferType :members: :undoc-members: @@ -73,6 +83,12 @@ Main Models The following models all contain their respective ``from_content`` methods. They all have their respective section in Tibia.com +BoostedCreature +--------------- +.. autoclass:: BoostedCreature + :members: + :inherited-members: + Character --------- .. autoclass:: Character diff --git a/tests/resources/tournaments/tournament_information_running.txt b/tests/resources/tournaments/tournament_information_running.txt new file mode 100644 index 00000000..5e667ea1 --- /dev/null +++ b/tests/resources/tournaments/tournament_information_running.txt @@ -0,0 +1,3 @@ +
+

Prove your Tibia skills!

Prove your skills by participating in Tournaments! Earn great rewards, such as Tournament Coins, Tibia Coins or Tournament Ticket Voucher by participating in a Tournament.

Fight hard and place yourself on top of the Tournament Leaderboard!

Your Tournament Information

Log in to see your personal Tournament information!


Tournament Details
Phase:running
Start Date:Jun 17 2019, 10:00:00 CEST
End Date:Jun 24 2019, 10:00:00 CEST
Worlds:Endebra, Endera, Endura, Velocera, Velocibra, Velocita
Rule Set:
PvP Type:Open PvP
Daily Tournament Playtime:02:00:00
Death Penalty Modifier:1.00x
XP Multiplier:7.00x
Skill Multiplier:10.00x
Spawn Rate Multiplier:1.00x
Loot Probability:1.00x
Rent Percentage:10%
House Auction Durations:0
Score Set:
Level Gain/Loss:+/- 100
Charm Point Multiplier (rare or lower creatures):Charm Points * 20
Reward Set:
Rank 1:3,0001,7501
Rank 2:2,0001,7501
Rank 3:1,0001,7501
Rank 4:7001,7501
Rank 5:5001,7501
Rank 6:4001,2501
Rank 7:3501,2501
Rank 8:3001,2501
Rank 9:2501,2501
Rank 10:2001,2501
Rank 11-15:1,0001
Rank 16-20:8001
Rank 21-25:7001
Rank 26-50:6001
Rank 51-75:500
Rank 76-100:450
Rank 101-125:350
Rank 126-150:300
Rank 151-175:250
Rank 176-200:150
Rank 201-99999:100

+ \ No newline at end of file diff --git a/tests/resources/tournaments/tournament_leaderboards_running.txt b/tests/resources/tournaments/tournament_leaderboards_running.txt new file mode 100644 index 00000000..37511b79 --- /dev/null +++ b/tests/resources/tournaments/tournament_leaderboards_running.txt @@ -0,0 +1,4 @@ +
+
Tournament Leaderboards
Here you can check the Tournament ranking of single characters on the single Tournament worlds as well as of the various Tournament cycles.

check this box to automatically refresh the leaderboard each minute
RankCharacterVocationScore
1. (+18) Himanentv TwitchSorcerer1,900
2. (+42) Mirosmar Barril DobradoSorcerer1,800
3. (+40) CaookDruid1,700
4. (+32) Doutor AurelioDruid1,700
5. (+29) Teus inSorcerer1,700
6. (+29) KywensKnight1,700
7. (+30) Rubinho OtitanPaladin1,700
8. (-4) Acid FujiDruid1,700
9. (+2) Rodriguez ChampionshipDruid1,600
10. (+30) MutsipSorcerer1,500
11. (+19) Lex Du GuesclinPaladin1,500
12. (+29) OpaladinPaladin1,400
13. (-12) Bacana TornezeiroSorcerer1,400
14. (+4) Falriah NathPaladin1,400
15. (+12) Pi KaSorcerer1,400
16. (-14) Esquilo Try HardDruid1,400
17. (+25) Algoz KeniKnight1,400
18. (+28) Nyderene IsunaKnight1,300
19. (+3) Dudric the GladiatorKnight1,300
20. (-8) Abitoxi EndebrandoSorcerer1,300
21. (-8) Druida Do VerdolaDruid1,300
22. (+29) IiePaladin1,200
23. (+33) Cohendo pro TrofeuPaladin1,200
24. (+30) MalestrixzsSorcerer1,200
25. (+28) Cidbom PainhoDruid1,200
26. (+5) Gaspar TournamentSorcerer1,200
27. (+28) Leo CluelessKnight1,200
28. (-8) Legendary XulodueKnight1,200
29. (+19) EsveinDruid1,100
30. (+19) AzaragothPaladin1,100
31. (-14) TranythzsSorcerer1,100
32. (-3) Solin SolinKnight1,100
33. (+14) AlestriderPaladin1,100
34. (+18) Leturk TiltDruid1,100
35. (-7) Andy el locoPaladin1,000
36. (+14) LanzadarPaladin1,000
37. (+2) Dive into GrindnessKnight1,000
38. (-15) Ramma AtenendyPaladin1,000
39. (-31) YadretSorcerer900
40. (-33) MarchelitoKnight900
41. (-25) MuufleszszKnight900
42. (-9) DyuzPaladin800
43. (-34) Menu MasSorcerer800
44. (-39) Zuke On FirePaladin800
45. (-35) Prion SainPaladin800
46. (-1) Wheels ChairKnight800
47. (-33) Galiazzi VanguardKnight800
48. (-24) Sonikast LavardKnight800
49. (-34) Paladino DasfrechaPaladin800
50. (-29) Portal GuyKnight800
51. (-26) Rock DirxKnight700
52. (-26) Ande PhindDruid600
53. (+6) ArrakiszPaladin600
54. (+6) Nervoso PoSorcerer600
55. (-17) Rata De TorneioPaladin600
56. (+2) ShionszzDruid600
57. (-25) Bernardo BurnquistKnight600
58. (-52) DaluzzzKnight500
59. (+3) DbrazzKnight300
60. (+3) Niix The BrutoKnight300
61. (-58) Lavawall cos PesKnight100
62. (-5) Crazy Of TournamentKnight0
63. (-2) Rubini TournamentSorcerer0
» Pages: 1
» Results: 63
+ + \ No newline at end of file diff --git a/tests/tests_client.py b/tests/tests_client.py index 5d10a5f3..21b3091c 100644 --- a/tests/tests_client.py +++ b/tests/tests_client.py @@ -13,7 +13,7 @@ from tests.tests_news import FILE_NEWS_LIST, FILE_NEWS_ARTICLE from tests.tests_tibiapy import TestCommons from tibiapy import Client, Character, Guild, Highscores, VocationFilter, Category, House, ListedHouse, ListedGuild, \ - KillStatistics, ListedNews, News, World, WorldOverview, Forbidden, NetworkError + KillStatistics, ListedNews, News, World, WorldOverview, Forbidden, NetworkError, BoostedCreature class TestClient(asynctest.TestCase, TestCommons): @@ -168,7 +168,13 @@ async def testFetchWorldList(self, mock): self.assertIsInstance(worlds, WorldOverview) + @aioresponses() + async def testFetchBoostedCreature(self, mock): + content = self._load_resource(self.FILE_UNRELATED_SECTION) + mock.get(News.get_list_url(), status=200, body=content) + creature = await self.client.fetch_boosted_creature() + self.assertIsInstance(creature, BoostedCreature) diff --git a/tests/tests_creature.py b/tests/tests_creature.py new file mode 100644 index 00000000..e9f439ca --- /dev/null +++ b/tests/tests_creature.py @@ -0,0 +1,20 @@ +import unittest + +from tests.tests_tibiapy import TestCommons +from tibiapy import BoostedCreature, InvalidContent + + +class TestCreature(TestCommons, unittest.TestCase): + # region Tibia.com Tests + def testBoostedCreature(self): + content = self._load_resource(self.FILE_UNRELATED_SECTION) + creature = BoostedCreature.from_content(content) + + self.assertIsInstance(creature, BoostedCreature) + self.assertEqual("Skeleton Warrior", creature.name) + + def testBoostedCreatureNotTibiaCom(self): + with self.assertRaises(InvalidContent): + BoostedCreature.from_content("

Nothing

") + + # endregion diff --git a/tests/tests_highscores.py b/tests/tests_highscores.py index ecdf6d58..66ecab40 100644 --- a/tests/tests_highscores.py +++ b/tests/tests_highscores.py @@ -98,6 +98,9 @@ def testHighscoresTibiaData(self): self.assertEqual(highscores.category, Category.AXE_FIGHTING) self.assertEqual(highscores.results_count, 300) + self.assertEqual(highscores.url_tibiadata, + Highscores.get_url_tibiadata(highscores.world, highscores.category, highscores.vocation)) + for entry in highscores.entries: self.assertIsInstance(entry, HighscoresEntry) self.assertIsInstance(entry.name, str) diff --git a/tests/tests_utils.py b/tests/tests_utils.py index 540d7a0e..780ddf61 100644 --- a/tests/tests_utils.py +++ b/tests/tests_utils.py @@ -5,6 +5,7 @@ from tests.tests_tibiapy import TestCommons from tibiapy import enums, utils +from tibiapy.utils import parse_integer, parse_tibia_money TIBIA_DATETIME_CEST = "Jul 10 2018, 07:13:32 CEST" TIBIA_DATETIME_CET = "Jan 10 2018, 07:13:32 CET" @@ -145,6 +146,7 @@ def testTryDateTime(self): def testParseNumberWords(self): self.assertEqual(utils.parse_number_words("one"), 1) + self.assertEqual(utils.parse_number_words("no"), 0) self.assertEqual(utils.parse_number_words("..."), 0) self.assertEqual(utils.parse_number_words("twenty-one"), 21) self.assertEqual(utils.parse_number_words("one hundred two"), 102) @@ -160,3 +162,18 @@ def testEnumStr(self): self.assertEqual(enums.VocationFilter.from_name("royal paladin"), enums.VocationFilter.PALADINS) self.assertEqual(enums.VocationFilter.from_name("unknown"), enums.VocationFilter.ALL) self.assertIsNone(enums.VocationFilter.from_name("unknown", False)) + + def testParseTibiaMoney(self): + self.assertEqual(1000, parse_tibia_money("1k")) + self.assertEqual(5000000, parse_tibia_money("5kk")) + self.assertEqual(2500, parse_tibia_money("2.5k")) + self.assertEqual(50, parse_tibia_money("50")) + with self.assertRaises(ValueError): + parse_tibia_money("abc") + + def testParseInteger(self): + self.assertEqual(1450, parse_integer("1.450")) + self.assertEqual(1110, parse_integer("1,110")) + self.assertEqual(15, parse_integer("15")) + self.assertEqual(0, parse_integer("abc")) + self.assertEqual(-1, parse_integer("abc", -1)) diff --git a/tests/tests_world.py b/tests/tests_world.py index c83cdfd6..d473f9d7 100644 --- a/tests/tests_world.py +++ b/tests/tests_world.py @@ -111,6 +111,8 @@ def testWorldOverview(self): self.assertGreater(world_overview.total_online, 0) self.assertIsNotNone(world_overview.record_date) self.assertIsNotNone(world_overview.record_count) + self.assertEqual(len(world_overview.regular_worlds), 65) + self.assertEqual(len(world_overview.tournament_worlds), 6) worlds = ListedWorld.list_from_content(content) self.assertEqual(len(world_overview.worlds), len(worlds)) @@ -206,6 +208,7 @@ def testWorldOverviewTibiaData(self): self.assertIsInstance(world_overview, WorldOverview) self.assertEqual(WorldOverview.get_url(), ListedWorld.get_list_url()) + self.assertEqual(WorldOverview.get_url_tibiadata(), ListedWorld.get_list_url_tibiadata()) self.assertGreater(sum(w.online_count for w in world_overview.worlds), 0) self.assertIsInstance(world_overview.worlds[0], ListedWorld) self.assertIsInstance(world_overview.worlds[0].pvp_type, PvpType) diff --git a/tibiapy/__init__.py b/tibiapy/__init__.py index a550effb..8d337824 100644 --- a/tibiapy/__init__.py +++ b/tibiapy/__init__.py @@ -10,9 +10,10 @@ from tibiapy.kill_statistics import * from tibiapy.news import * from tibiapy.world import * +from tibiapy.creature import * from tibiapy.client import * -__version__ = '2.0.1' +__version__ = '2.1.0' from logging import NullHandler diff --git a/tibiapy/abc.py b/tibiapy/abc.py index 9eb308a6..ebc20978 100644 --- a/tibiapy/abc.py +++ b/tibiapy/abc.py @@ -5,13 +5,13 @@ from collections import OrderedDict from enum import Enum -from tibiapy.enums import HouseType +from tibiapy.enums import HouseType, HouseStatus, HouseOrder CHARACTER_URL = "https://www.tibia.com/community/?subtopic=characters&name=%s" CHARACTER_URL_TIBIADATA = "https://api.tibiadata.com/v2/characters/%s.json" HOUSE_URL = "https://www.tibia.com/community/?subtopic=houses&page=view&houseid=%d&world=%s" HOUSE_URL_TIBIADATA = "https://api.tibiadata.com/v2/house/%s/%d.json" -HOUSE_LIST_URL = "https://www.tibia.com/community/?subtopic=houses&world=%s&town=%s&type=%s" +HOUSE_LIST_URL = "https://www.tibia.com/community/?subtopic=houses&world=%s&town=%s&type=%s&status=%s&order=%s" HOUSE_LIST_URL_TIBIADATA = "https://api.tibiadata.com/v2/houses/%s/%s/%s.json" GUILD_URL = "https://www.tibia.com/community/?subtopic=guilds&page=view&GuildName=%s" GUILD_URL_TIBIADATA = "https://api.tibiadata.com/v2/guild/%s.json" @@ -321,7 +321,8 @@ def get_url_tibiadata(cls, house_id, world): return HOUSE_URL_TIBIADATA % (world, house_id) @classmethod - def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE): + def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE, status: HouseStatus = None, + order=HouseOrder.NAME): """ Gets the URL to the house list on Tibia.com with the specified parameters. @@ -333,6 +334,10 @@ def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE): The name of the town. house_type: :class:`HouseType` Whether to search for houses or guildhalls. + status: :class:`HouseStatus`, optional + The house status to filter results. By default no filters will be applied. + order: :class:`HouseOrder`, optional + The ordering to use for the results. By default they are sorted by name. Returns ------- @@ -340,7 +345,8 @@ def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE): The URL to the list matching the parameters. """ house_type = "%ss" % house_type.value - return HOUSE_LIST_URL % (urllib.parse.quote(world), urllib.parse.quote(town), house_type) + status = "" if status is None else status.value + return HOUSE_LIST_URL % (urllib.parse.quote(world), urllib.parse.quote(town), house_type, status, order.value) @classmethod def get_list_url_tibiadata(cls, world, town, house_type: HouseType = HouseType.HOUSE): diff --git a/tibiapy/client.py b/tibiapy/client.py index 3b526935..b3824a49 100644 --- a/tibiapy/client.py +++ b/tibiapy/client.py @@ -5,7 +5,8 @@ import tibiapy from tibiapy import Character, Guild, World, House, KillStatistics, ListedGuild, Highscores, Category, VocationFilter, \ - ListedHouse, HouseType, WorldOverview, NewsCategory, NewsType, ListedNews, News, Forbidden, NetworkError + ListedHouse, HouseType, WorldOverview, NewsCategory, NewsType, ListedNews, News, Forbidden, NetworkError, \ + HouseStatus, HouseOrder, BoostedCreature __all__ = ( "Client", @@ -37,11 +38,14 @@ def __init__(self, loop=None, session=None): self.loop.create_task(self._initialize_session()) async def _initialize_session(self): - self.session = aiohttp.ClientSession(loop=self.loop) # type: aiohttp.ClientSession + headers = { + 'User-Agent ': "Tibia.py/%s (+https://github.com/Galarzaa90/tibia.py" % tibiapy.__version__ + } + self.session = aiohttp.ClientSession(loop=self.loop, headers=headers) # type: aiohttp.ClientSession @classmethod def _handle_status(cls, status_code): - """Handles error status codes, raising exceptions if neccesary.""" + """Handles error status codes, raising exceptions if necessary.""" if status_code < 400: return if status_code == 403: @@ -99,6 +103,28 @@ async def _post(self, url, data): except aiohttp.ClientError as e: raise NetworkError("aiohttp.ClientError: %s" % e, e) + async def fetch_boosted_creature(self): + """Fetches today's boosted creature. + + .. versionadded:: 2.1.0 + + Returns + ------- + :class:`BoostedCreature` + The boosted creature of the day. + + Raises + ------ + Forbidden + If a 403 Forbidden error was returned. + This usually means that Tibia.com is rate-limiting the client because of too many requests. + NetworkError + If there's any connection errors during the request. + """ + content = await self._get(News.get_list_url()) + boosted_creature = BoostedCreature.from_content(content) + return boosted_creature + async def fetch_character(self, name): """Fetches a character by its name from Tibia.com @@ -258,7 +284,8 @@ async def fetch_world(self, name): world = World.from_content(content) return world - async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE): + async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE, status: HouseStatus = None, + order=HouseOrder.NAME): """Fetches the house list of a world and type. Parameters @@ -269,6 +296,10 @@ async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE): The name of the town. house_type: :class:`HouseType` The type of building. House by default. + status: :class:`HouseStatus`, optional + The house status to filter results. By default no filters will be applied. + order: :class:`HouseOrder`, optional + The ordering to use for the results. By default they are sorted by name. Returns ------- @@ -283,7 +314,7 @@ async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE): NetworkError If there's any connection errors during the request. """ - content = await self._get(ListedHouse.get_list_url(world, town, house_type)) + content = await self._get(ListedHouse.get_list_url(world, town, house_type, status, order)) houses = ListedHouse.list_from_content(content) return houses @@ -291,7 +322,7 @@ async def fetch_world_guilds(self, world: str): """Fetches the list of guilds in a world from Tibia.com Parameters - ---------- + ---------cl- world: :class:`str` The name of the world. diff --git a/tibiapy/creature.py b/tibiapy/creature.py new file mode 100644 index 00000000..1d5d7e65 --- /dev/null +++ b/tibiapy/creature.py @@ -0,0 +1,67 @@ +import bs4 + +from tibiapy import abc, InvalidContent + +__all__ = ( + "BoostedCreature", +) + +BOOSTED_ALT = "Today's boosted creature: " + + +class BoostedCreature(abc.Serializable): + """Represents a boosted creature entry. + + This creature changes every server save and applies to all Game Worlds. + Boosted creatures yield twice the amount of experience points, carry more loot and respawn at a faster rate. + + Attributes + ---------- + name: :class:`str` + The name of the boosted creature. + image_url: :class:`str` + An URL containing the boosted creature's image. + """ + __slots__ = ( + "name", + "image_url", + ) + + def __init__(self, name, image_url): + self.name = name + self.image_url = image_url + + def __repr__(self): + return "<{0.__class__.__name__} name={0.name!r} image_url={0.image_url!r}>".format(self) + + @classmethod + def from_content(cls, content): + """ + Gets the boosted creature from any Tibia.com page. + + + Parameters + ---------- + content: :class:`str` + The HTML content of a Tibia.com page. + + Returns + ------- + :class:`News` + The boosted article shown. + + Raises + ------ + InvalidContent + If content is not the HTML of a Tibia.com's page. + """ + try: + parsed_content = bs4.BeautifulSoup(content.replace('ISO-8859-1', 'utf-8'), "lxml", + parse_only=bs4.SoupStrainer("div", attrs={"id": "RightArtwork"})) + img = parsed_content.find("img", attrs={"id": "Monster"}) + name = img["title"].replace(BOOSTED_ALT, "").strip() + image_url = img["src"] + return cls(name, image_url) + except TypeError: + raise InvalidContent("content is not from Tibia.com") + diff --git a/tibiapy/enums.py b/tibiapy/enums.py index f3fa86f5..8414d90d 100644 --- a/tibiapy/enums.py +++ b/tibiapy/enums.py @@ -3,6 +3,7 @@ __all__ = ( 'AccountStatus', 'Category', + 'HouseOrder', 'HouseStatus', 'HouseType', 'NewsCategory', @@ -50,6 +51,15 @@ class Category(BaseEnum): SWORD_FIGHTING = "sword" +class HouseOrder(BaseEnum): + """The possible ordering methods for house lists in Tibia.com""" + NAME = "name" + SIZE = "size" + RENT = "rent" + BID = "bid" + AUCTION_END = "end" + + class HouseStatus(BaseEnum): """Renting statuses of a house.""" RENTED = "rented" diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py index b2838657..502edbe2 100644 --- a/tibiapy/highscores.py +++ b/tibiapy/highscores.py @@ -187,8 +187,7 @@ def from_tibiadata(cls, content, vocation=None): highscores.results_count = len(highscores.entries) except KeyError: raise InvalidContent("content is not a TibiaData highscores response.") - if isinstance(vocation, VocationFilter): - highscores.vocation = vocation + highscores.vocation = vocation or VocationFilter.ALL return highscores @classmethod diff --git a/tibiapy/house.py b/tibiapy/house.py index 96419bda..ec3ca896 100644 --- a/tibiapy/house.py +++ b/tibiapy/house.py @@ -174,12 +174,12 @@ def from_content(cls, content): house.id = int(id_regex.search(house.image_url).group(1)) m = bed_regex.search(beds) if m: - house.type = HouseType.GUILDHALL if m.group("type") in ["guildhall", "clanhall"] else HouseType.HOUSE - beds_word = m.group("beds") - if beds_word == "no": - house.beds = 0 + if m.group("type").lower() in ["guildhall", "clanhall"]: + house.type = HouseType.GUILDHALL else: - house.beds = parse_number_words(beds_word) + house.type = HouseType.HOUSE + beds_word = m.group("beds") + house.beds = parse_number_words(beds_word) m = info_regex.search(info) if m: diff --git a/tibiapy/utils.py b/tibiapy/utils.py index d6092539..4198f93a 100644 --- a/tibiapy/utils.py +++ b/tibiapy/utils.py @@ -10,6 +10,29 @@ TIBIA_CASH_PATTERN = re.compile(r'(\d*\.?\d*)k*$') +def parse_integer(number: str, default=0): + """Parses a string representing an integer, ignoring commas or periods. + + Parameters + ---------- + number: :class:`str` + A string representing a number. + default: :class:`int` + The default value to use if the string is not numeric. + By default, 0 is used. + + Returns + ------- + :class:`int` + The represented integer, or the default value if invalid. + """ + try: + number = re.sub(r'[,.]', '', number.strip()) + return int(number) + except ValueError: + return default + + def parse_tibia_datetime(datetime_str) -> Optional[datetime.datetime]: """Parses date and time from the format used in Tibia.com diff --git a/tibiapy/world.py b/tibiapy/world.py index 145c1b18..17bd48c9 100644 --- a/tibiapy/world.py +++ b/tibiapy/world.py @@ -8,7 +8,7 @@ from tibiapy.character import OnlineCharacter from tibiapy.enums import PvpType, TransferType, WorldLocation from tibiapy.utils import parse_json, parse_tibia_datetime, parse_tibia_full_date, parse_tibiacom_content, \ - parse_tibiadata_datetime, try_date, try_datetime, try_enum + parse_tibiadata_datetime, try_date, try_datetime, try_enum, parse_integer __all__ = ( "ListedWorld", @@ -349,7 +349,7 @@ def _parse_world_info(self, world_info_table): self.transfer_type = try_enum(TransferType, world_info.pop("transfer_type", None), TransferType.REGULAR) m = record_regexp.match(world_info.pop("online_record")) if m: - self.record_count = int(m.group("count").replace(".", "")) + self.record_count = parse_integer(m.group("count")) self.record_date = parse_tibia_datetime(m.group("date")) if "world_quest_titles" in world_info: self.world_quest_titles = [q.strip() for q in world_info.pop("world_quest_titles").split(",")] @@ -444,6 +444,18 @@ def total_online(self): """:class:`int`: Total players online across all worlds.""" return sum(w.online_count for w in self.worlds) + @property + def tournament_worlds(self): + """:class:`list` of :class:`GuildMember`: List of tournament worlds. + + Note that tournament worlds are not listed when there are no active or upcoming tournaments.""" + return [w for w in self.worlds if w.tournament_world_type is not None] + + @property + def regular_worlds(self): + """:class:`list` of :class:`ListedWorld`: List of worlds that are not tournament worlds.""" + return [w for w in self.worlds if w.tournament_world_type is None] + @classmethod def get_url(cls): """ @@ -492,9 +504,7 @@ def from_content(cls, content): try: record_row, *rows = parsed_content.find_all("tr") m = record_regexp.search(record_row.text) - if not m: - raise InvalidContent("content does not belong to the World Overview section in Tibia.com") - world_overview.record_count = int(m.group("count").replace(".", "")) + world_overview.record_count = parse_integer(m.group("count")) world_overview.record_date = parse_tibia_datetime(m.group("date")) world_rows = rows world_overview._parse_worlds(world_rows)