From 6a32780b4181b31bb543f6742677bf530c648c30 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Sat, 5 Jan 2019 13:41:41 -0700 Subject: [PATCH 01/12] Added skeleton for Highscores parsing --- tibiapy/highscores.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tibiapy/highscores.py diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py new file mode 100644 index 00000000..3ff322ad --- /dev/null +++ b/tibiapy/highscores.py @@ -0,0 +1,19 @@ +from tibiapy import abc + + +class Highscores(abc.Serializable): + def __init__(self, world, category, **kwargs): + self.world = world + self.category = category + self.vocation = kwargs.get("vocation", "all") + self.entries = kwargs.get("entries", []) + self.results_count = kwargs.get("results_count") + + +class HighscoresEntry(abc.BaseCharacter): + def __init__(self, name, rank, vocation, value, extra=None): + self.name = name + self.rank = rank + self.vocation = vocation + self.value = value + self.extra = extra From 004f5aa8963d262e18bcda3b93caf208421d634b Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Sun, 6 Jan 2019 11:25:48 -0700 Subject: [PATCH 02/12] Parse world, categories and vocation for Highscores --- tests/resources/highscores/tibiacom_full.txt | 8 +++ tests/tests_highscores.py | 18 +++++++ tibiapy/__init__.py | 1 + tibiapy/enums.py | 24 ++++++++- tibiapy/highscores.py | 54 ++++++++++++++++++-- 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/resources/highscores/tibiacom_full.txt create mode 100644 tests/tests_highscores.py diff --git a/tests/resources/highscores/tibiacom_full.txt b/tests/resources/highscores/tibiacom_full.txt new file mode 100644 index 00000000..e10b7dd1 --- /dev/null +++ b/tests/resources/highscores/tibiacom_full.txt @@ -0,0 +1,8 @@ +
+
Highscores Filter
World:
Vocation:
Category:

Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).

Highscores
RankNameVocationLevel
1SkogisElite Knight13
2Sebbe Da NukerElite Knight12
3Dark AmmbaElite Knight12
4ExorinhoElite Knight12
5WivivolElite Knight12
6Dekos WakuseiElite Knight12
7GerkulesElite Knight12
8Kitty MorganaElite Knight12
9Sir IgoroElite Knight12
10Bomin KenilElite Knight12
11EndrianoKnight12
12Pappa RockyElite Knight12
13RamboartosElite Knight12
14QarmaElite Knight11
15Rezuv TankerKnight11
16Aragorn ElessarKnight11
17Zlaten IbrahimovicKnight11
18Dancer ClubElite Knight11
19Nameck KnightKnight11
20LuthreckElite Knight11
21AvilazinhoKnight11
22AerpionKnight11
23Jeffrey KintaElite Knight11
24Enyx DeathweaverElite Knight11
25Lucky PujjhardElite Knight11
» Pages: 1 2 3 4 5 6 7 8 9 10 11 12
» Results: 300
+ + +
+
+
+ \ No newline at end of file diff --git a/tests/tests_highscores.py b/tests/tests_highscores.py new file mode 100644 index 00000000..b22e7b0e --- /dev/null +++ b/tests/tests_highscores.py @@ -0,0 +1,18 @@ +from tests.tests_tibiapy import TestTibiaPy +from tibiapy import Highscores, VocationFilter, Category + +FILE_HIGHSCORES_FULL = "highscores/tibiacom_full.txt" + + +class TestWorld(TestTibiaPy): + + # region Tibia.com Tests + def testWorld(self): + content = self._load_resource(FILE_HIGHSCORES_FULL) + highscores = Highscores.from_content(content) + + self.assertEqual(highscores.world, "Estela") + self.assertEqual(highscores.vocation, VocationFilter.KNIGHTS) + self.assertEqual(highscores.category, Category.MAGIC_LEVEL) + + # endregion diff --git a/tibiapy/__init__.py b/tibiapy/__init__.py index 68af4d33..b6bd0770 100644 --- a/tibiapy/__init__.py +++ b/tibiapy/__init__.py @@ -3,6 +3,7 @@ from tibiapy.enums import * from tibiapy.errors import * from tibiapy.guild import * +from tibiapy.highscores import * from tibiapy.house import * from tibiapy.world import * diff --git a/tibiapy/enums.py b/tibiapy/enums.py index eaacee2a..4c02bbd3 100644 --- a/tibiapy/enums.py +++ b/tibiapy/enums.py @@ -1,6 +1,7 @@ from enum import Enum -__all__ = ('AccountStatus', 'HouseStatus', 'HouseType', 'PvpType', 'Sex', 'TransferType', 'Vocation', 'WorldLocation') +__all__ = ('AccountStatus', 'Category', 'HouseStatus', 'HouseType', 'PvpType', 'Sex', 'TransferType', 'Vocation', + 'VocationFilter', 'WorldLocation') class BaseEnum(Enum): @@ -14,6 +15,19 @@ class AccountStatus(BaseEnum): PREMIUM_ACCOUNT = "Premium Account" +class Category(BaseEnum): + ACHIEVEMENTS = "achievements" + AXE_FIGHTING = "axe" + CLUB_FIGHTING = "club" + DISTANCE_FIGHTING = "distance" + EXPERIENCE = "experience" + FISHING = "fishing" + FIST_FIGHTING = "fist" + LOYALTY_POINTS = "loyalty" + MAGIC_LEVEL = "magic" + SWORD_FIGHTING = "sword" + + class HouseStatus(BaseEnum): """Renting statuses of a house.""" RENTED = "rented" @@ -63,6 +77,14 @@ class Vocation(BaseEnum): MASTER_SORCERER = "Master Sorcerer" +class VocationFilter(Enum): + ALL = 0 + KNIGHTS = 1 + PALADINS = 2 + SORCERERS = 3 + DRUIDS = 4 + + class WorldLocation(BaseEnum): """The possible physical locations for servers.""" EUROPE = "Europe" diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py index 3ff322ad..8a3ab611 100644 --- a/tibiapy/highscores.py +++ b/tibiapy/highscores.py @@ -1,14 +1,62 @@ -from tibiapy import abc +from collections import OrderedDict + +from tibiapy import abc, InvalidContent, VocationFilter, Category +from tibiapy.utils import parse_tibiacom_content, try_enum + +__all__ = ("Highscores", "HighscoresEntry") class Highscores(abc.Serializable): def __init__(self, world, category, **kwargs): self.world = world - self.category = category - self.vocation = kwargs.get("vocation", "all") + self.category = try_enum(Category, category) + self.vocation = try_enum(VocationFilter, kwargs.get("vocation"), VocationFilter.ALL) self.entries = kwargs.get("entries", []) self.results_count = kwargs.get("results_count") + @classmethod + def from_content(cls, content): + parsed_content = parse_tibiacom_content(content) + tables = cls._parse_tables(parsed_content) + filters = tables.get("Highscores Filter") + if filters is None: + raise InvalidContent() + world_filter, vocation_filter, category_filter = filters + world = world_filter.find("option", {"selected": True})["value"] + vocation = int(vocation_filter.find("option", {"selected": True})["value"]) + category = category_filter.find("option", {"selected": True})["value"] + highscores = cls(world, category, vocation=vocation) + entries = tables.get("Highscores") + if entries is None: + return None + return highscores + + + @classmethod + def _parse_tables(cls, parsed_content): + """ + Parses the information tables found in a world's information page. + + Parameters + ---------- + parsed_content: :class:`bs4.BeautifulSoup` + A :class:`BeautifulSoup` object containing all the content. + + Returns + ------- + :class:`OrderedDict`[:class:`str`, :class:`list`[:class:`bs4.Tag`]] + A dictionary containing all the table rows, with the table headers as keys. + """ + tables = parsed_content.find_all('div', attrs={'class': 'TableContainer'}) + output = OrderedDict() + for table in tables: + title = table.find("div", attrs={'class': 'Text'}).text + title = title.split("[")[0].strip() + inner_table = table.find("div", attrs={'class': 'InnerTableContainer'}) + output[title] = inner_table.find_all("tr") + return output + # endregion + class HighscoresEntry(abc.BaseCharacter): def __init__(self, name, rank, vocation, value, extra=None): From 3e10e94f6e10ed5100370b3e2fa774c3ade4d626 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Sun, 6 Jan 2019 13:35:09 -0700 Subject: [PATCH 03/12] Parsing highscore entries --- .../highscores/tibiacom_experience.txt | 4 +++ .../resources/highscores/tibiacom_loyalty.txt | 3 ++ tests/tests_highscores.py | 20 ++++++++++++- tibiapy/highscores.py | 29 ++++++++++++++++--- 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 tests/resources/highscores/tibiacom_experience.txt create mode 100644 tests/resources/highscores/tibiacom_loyalty.txt diff --git a/tests/resources/highscores/tibiacom_experience.txt b/tests/resources/highscores/tibiacom_experience.txt new file mode 100644 index 00000000..114b19ad --- /dev/null +++ b/tests/resources/highscores/tibiacom_experience.txt @@ -0,0 +1,4 @@ +
+
Highscores Filter
World:
Vocation:
Category:

Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).

Highscores
RankNameVocationLevelPoints
1JahcureRoyal Paladin7878,083,013,963
2Sir Fenix GalaxyRoyal Paladin7607,263,651,916
3Ragnar IncRoyal Paladin7597,254,728,435
4Show danRoyal Paladin7276,372,307,887
5Hell PumaRoyal Paladin6995,652,173,922
6Kirito MitoRoyal Paladin6905,448,964,469
7Nostro AdemusRoyal Paladin6895,424,728,234
8NaastridRoyal Paladin6614,777,584,954
9BeboyyRoyal Paladin6604,761,690,897
10Aght TerinaRoyal Paladin6344,213,832,350
11FreestylinRoyal Paladin6213,957,553,125
12Lapa Da TankRoyal Paladin6193,927,591,080
13Moreno FideraRoyal Paladin6153,842,085,201
14Akarians DemonsRoyal Paladin6103,750,276,607
15Rich VenturaRoyal Paladin5933,451,884,136
16Mad Woman FailRoyal Paladin5893,373,205,570
17RahkiPaladin5883,360,919,357
18Mi NamelessRoyal Paladin5843,290,870,583
19Hell LancerRoyal Paladin5783,185,146,346
20HaojrsRoyal Paladin5763,164,770,018
21Lord of CoxinhaRoyal Paladin5743,123,723,539
22SalladixRoyal Paladin5703,063,862,317
23EzuduxRoyal Paladin5532,792,304,037
24DoggydigRoyal Paladin5452,669,705,616
25ToxikowPaladin5352,523,902,188
» Pages: 1 2 3 4 5 6 7 8 9 10 11 12
» Results: 300
+ + \ No newline at end of file diff --git a/tests/resources/highscores/tibiacom_loyalty.txt b/tests/resources/highscores/tibiacom_loyalty.txt new file mode 100644 index 00000000..881262ba --- /dev/null +++ b/tests/resources/highscores/tibiacom_loyalty.txt @@ -0,0 +1,3 @@ +
+
Highscores Filter
World:
Vocation:
Category:

Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).

Highscores
RankNameVocationTitlePoints
1ScrumdiliumpiousRoyal PaladinSage of Tibia5,608
2DawoxRoyal PaladinSage of Tibia5,227
3Alexzander The GreatRoyal PaladinSage of Tibia5,110
4TaziseRoyal PaladinSage of Tibia5,108
5MickeysRoyal PaladinSage of Tibia5,011
6La FargePaladinGuardian of Tibia4,929
7Maria TerezaPaladinGuardian of Tibia4,839
8Ninja BetaRoyal PaladinGuardian of Tibia4,684
9AzazielRoyal PaladinGuardian of Tibia4,554
10Sara NithPaladinGuardian of Tibia4,465
11TokfiaRoyal PaladinGuardian of Tibia4,449
12Chrome CougarRoyal PaladinGuardian of Tibia4,324
13SejalzorRoyal PaladinGuardian of Tibia4,202
14Sir DzidekRoyal PaladinGuardian of Tibia4,183
15Kaly ShieRoyal PaladinGuardian of Tibia4,119
16Amber WolfhavenPaladinGuardian of Tibia4,097
17MauslayerRoyal PaladinGuardian of Tibia4,056
18Yathavek SapervarusPaladinKeeper of Tibia3,973
19Loyal Paladin Of CalmeraRoyal PaladinKeeper of Tibia3,948
20Shanix PoxaPaladinKeeper of Tibia3,922
21Neon HeroPaladinKeeper of Tibia3,886
22CantinelleRoyal PaladinKeeper of Tibia3,885
23Greens ArrowRoyal PaladinKeeper of Tibia3,853
24RawmanPaladinKeeper of Tibia3,765
25RocktronRoyal PaladinKeeper of Tibia3,631
» Pages: 1 2 3
» Results: 65
+ \ No newline at end of file diff --git a/tests/tests_highscores.py b/tests/tests_highscores.py index b22e7b0e..19f84aa9 100644 --- a/tests/tests_highscores.py +++ b/tests/tests_highscores.py @@ -2,12 +2,14 @@ from tibiapy import Highscores, VocationFilter, Category FILE_HIGHSCORES_FULL = "highscores/tibiacom_full.txt" +FILE_HIGHSCORES_EXPERIENCE = "highscores/tibiacom_experience.txt" +FILE_HIGHSCORES_LOYALTY = "highscores/tibiacom_loyalty.txt" class TestWorld(TestTibiaPy): # region Tibia.com Tests - def testWorld(self): + def testHighscores(self): content = self._load_resource(FILE_HIGHSCORES_FULL) highscores = Highscores.from_content(content) @@ -15,4 +17,20 @@ def testWorld(self): self.assertEqual(highscores.vocation, VocationFilter.KNIGHTS) self.assertEqual(highscores.category, Category.MAGIC_LEVEL) + def testHighscoresExperience(self): + content = self._load_resource(FILE_HIGHSCORES_EXPERIENCE) + highscores = Highscores.from_content(content) + + self.assertEqual(highscores.world, "Gladera") + self.assertEqual(highscores.vocation, VocationFilter.PALADINS) + self.assertEqual(highscores.category, Category.EXPERIENCE) + + def testHighscoresLoyalty(self): + content = self._load_resource(FILE_HIGHSCORES_LOYALTY) + highscores = Highscores.from_content(content) + + self.assertEqual(highscores.world, "Calmera") + self.assertEqual(highscores.vocation, VocationFilter.PALADINS) + self.assertEqual(highscores.category, Category.LOYALTY_POINTS) + # endregion diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py index 8a3ab611..1b7ab810 100644 --- a/tibiapy/highscores.py +++ b/tibiapy/highscores.py @@ -1,10 +1,13 @@ +import re from collections import OrderedDict -from tibiapy import abc, InvalidContent, VocationFilter, Category +from tibiapy import abc, InvalidContent, VocationFilter, Category, Vocation from tibiapy.utils import parse_tibiacom_content, try_enum __all__ = ("Highscores", "HighscoresEntry") +results_pattern = re.compile(r'Results: (\d+)') + class Highscores(abc.Serializable): def __init__(self, world, category, **kwargs): @@ -14,6 +17,7 @@ def __init__(self, world, category, **kwargs): self.entries = kwargs.get("entries", []) self.results_count = kwargs.get("results_count") + @classmethod def from_content(cls, content): parsed_content = parse_tibiacom_content(content) @@ -29,6 +33,13 @@ def from_content(cls, content): entries = tables.get("Highscores") if entries is None: return None + _, header, *rows = entries + info_row = rows.pop() + highscores.results_count = int(results_pattern.search(info_row.text).group(1)) + for row in rows: + cols_raw = row.find_all('td') + cols_clean = [c.text.replace('\xa0', ' ').strip() for c in cols_raw] + highscores.parse_entry(cols_clean) return highscores @@ -57,11 +68,21 @@ def _parse_tables(cls, parsed_content): return output # endregion + def parse_entry(self, cols_clean): + rank, name, vocation, *values = cols_clean + rank = int(rank) + if self.category == Category.EXPERIENCE or self.category == Category.LOYALTY_POINTS: + _, value = values + else: + value, *_ = values + value = int(value.replace(',', '')) + entry = HighscoresEntry(name, rank, vocation, value) + self.entries.append(entry) + class HighscoresEntry(abc.BaseCharacter): - def __init__(self, name, rank, vocation, value, extra=None): + def __init__(self, name, rank, vocation, value): self.name = name self.rank = rank - self.vocation = vocation + self.vocation = try_enum(Vocation, vocation) self.value = value - self.extra = extra From f2cc3fdf46b3680dc7aae8d4d178699411cfda22 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 7 Jan 2019 14:44:36 -0700 Subject: [PATCH 04/12] Completed Tibia.com parsing for Highscores - Added type hint for Character variables - Added docstring for Category and VocationFilter enums - Documentation's sidebar is now fixed to the screen. --- .travis.yml | 5 +- docs/api.rst | 33 ++++ docs/conf.py | 6 +- tests/resources/highscores/tibiacom_empty.txt | 3 + tests/tests_highscores.py | 39 +++- tibiapy/character.py | 21 +- tibiapy/enums.py | 2 + tibiapy/highscores.py | 185 ++++++++++++++++-- tibiapy/utils.py | 4 +- 9 files changed, 263 insertions(+), 35 deletions(-) create mode 100644 tests/resources/highscores/tibiacom_empty.txt diff --git a/.travis.yml b/.travis.yml index 617bd4e0..ae0d31cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ cache: pip install: - pip install -U -r requirements.txt - pip install -U setuptools wheel - - pip install coverage + - pip install coverage codecov - pip install -U Sphinx before_script: @@ -25,9 +25,10 @@ script: - make html - cd .. -after_script: +after_sucess: - coverage report - coverage xml + - codecov - if [[ "$TRAVIS_PULL_REQUEST" == "false" && "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi deploy: diff --git a/docs/api.rst b/docs/api.rst index 97842a9b..e3d34d56 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -20,6 +20,10 @@ Enumerations are provided for various values in order to avoid depending on stri :members: :undoc-members: +.. autoclass:: Category + :members: + :undoc-members: + .. autoclass:: HouseStatus :members: :undoc-members: @@ -40,6 +44,10 @@ Enumerations are provided for various values in order to avoid depending on stri :members: :undoc-members: +.. autoclass:: VocationFilter + :members: + :undoc-members: + .. autoclass:: WorldLocation :members: :undoc-members: @@ -61,6 +69,12 @@ Guild :members: :inherited-members: +Highscores +---------- +.. autoclass:: Highscores + :members: + :inherited-members: + House ----- .. autoclass:: House @@ -120,6 +134,12 @@ CharacterHouse :members: :inherited-members: +ExpHighscoresEntry +------------------------- +.. autoclass:: ExpHighscoresEntry + :members: + :inherited-members: + Death ----- .. autoclass:: Death @@ -150,12 +170,25 @@ GuildMembership :members: :inherited-members: + +HighscoresEntry +--------------- +.. autoclass:: HighscoresEntry + :members: + :inherited-members: + Killer ------ .. autoclass:: Killer :members: :inherited-members: +LoyaltyHighscoresEntry +---------------------- +.. autoclass:: LoyaltyHighscoresEntry + :members: + :inherited-members: + OnlineCharacter --------------- .. autoclass:: OnlineCharacter diff --git a/docs/conf.py b/docs/conf.py index 5baa2ed3..a2b698b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -96,9 +96,11 @@ def setup(app): # documentation. # html_theme_options = { - 'github_user': 'galarzaa90', + 'github_user': 'Galarzaa90', 'github_repo': 'tibia.py', - 'github_type': 'star' + 'github_type': 'star', + 'fixed_sidebar': True, + 'travis_button': True } # Add any paths that contain custom static files (such as style sheets) here, diff --git a/tests/resources/highscores/tibiacom_empty.txt b/tests/resources/highscores/tibiacom_empty.txt new file mode 100644 index 00000000..fdd8811e --- /dev/null +++ b/tests/resources/highscores/tibiacom_empty.txt @@ -0,0 +1,3 @@ +
+
Highscores Filter
World:
Vocation:
Category:

Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).

+ \ No newline at end of file diff --git a/tests/tests_highscores.py b/tests/tests_highscores.py index 19f84aa9..c75844f6 100644 --- a/tests/tests_highscores.py +++ b/tests/tests_highscores.py @@ -1,13 +1,14 @@ from tests.tests_tibiapy import TestTibiaPy -from tibiapy import Highscores, VocationFilter, Category +from tibiapy import Category, ExpHighscoresEntry, Highscores, HighscoresEntry, LoyaltyHighscoresEntry, Vocation, \ + VocationFilter FILE_HIGHSCORES_FULL = "highscores/tibiacom_full.txt" FILE_HIGHSCORES_EXPERIENCE = "highscores/tibiacom_experience.txt" FILE_HIGHSCORES_LOYALTY = "highscores/tibiacom_loyalty.txt" +FILE_HIGHSCORES_EMPTY = "highscores/tibiacom_empty.txt" -class TestWorld(TestTibiaPy): - +class TestHighscores(TestTibiaPy): # region Tibia.com Tests def testHighscores(self): content = self._load_resource(FILE_HIGHSCORES_FULL) @@ -16,6 +17,14 @@ def testHighscores(self): self.assertEqual(highscores.world, "Estela") self.assertEqual(highscores.vocation, VocationFilter.KNIGHTS) self.assertEqual(highscores.category, Category.MAGIC_LEVEL) + self.assertEqual(highscores.results_count, 300) + + for entry in highscores.entries: + self.assertIsInstance(entry, HighscoresEntry) + self.assertIsInstance(entry.name, str) + self.assertIsInstance(entry.vocation, Vocation) + self.assertIsInstance(entry.rank, int) + self.assertIsInstance(entry.value, int) def testHighscoresExperience(self): content = self._load_resource(FILE_HIGHSCORES_EXPERIENCE) @@ -24,6 +33,15 @@ def testHighscoresExperience(self): self.assertEqual(highscores.world, "Gladera") self.assertEqual(highscores.vocation, VocationFilter.PALADINS) self.assertEqual(highscores.category, Category.EXPERIENCE) + self.assertEqual(highscores.results_count, 300) + + for entry in highscores.entries: + self.assertIsInstance(entry, ExpHighscoresEntry) + self.assertIsInstance(entry.name, str) + self.assertIsInstance(entry.vocation, Vocation) + self.assertIsInstance(entry.rank, int) + self.assertIsInstance(entry.value, int) + self.assertIsInstance(entry.level, int) def testHighscoresLoyalty(self): content = self._load_resource(FILE_HIGHSCORES_LOYALTY) @@ -32,5 +50,20 @@ def testHighscoresLoyalty(self): self.assertEqual(highscores.world, "Calmera") self.assertEqual(highscores.vocation, VocationFilter.PALADINS) self.assertEqual(highscores.category, Category.LOYALTY_POINTS) + self.assertEqual(highscores.results_count, 65) + + for entry in highscores.entries: + self.assertIsInstance(entry, LoyaltyHighscoresEntry) + self.assertIsInstance(entry.name, str) + self.assertIsInstance(entry.vocation, Vocation) + self.assertIsInstance(entry.rank, int) + self.assertIsInstance(entry.value, int) + self.assertIsInstance(entry.title, str) + + def testHighscoresEmpty(self): + content = self._load_resource(FILE_HIGHSCORES_EMPTY) + highscores = Highscores.from_content(content) + + self.assertIsNone(highscores) # endregion diff --git a/tibiapy/character.py b/tibiapy/character.py index 6c023d68..c069b761 100644 --- a/tibiapy/character.py +++ b/tibiapy/character.py @@ -131,23 +131,22 @@ class Character(abc.BaseCharacter): "achievements", "deaths", "account_information", "other_characters", "deletion_date") def __init__(self, name=None, world=None, vocation=None, level=0, sex=None, **kwargs): - self.name = name - self.former_names = kwargs.get("former_names", []) + self.name = name # type: str + self.former_names = kwargs.get("former_names", []) # type: List[str] self.sex = try_enum(Sex, sex) self.vocation = try_enum(Vocation, vocation) - self.level = level - self.achievement_points = kwargs.get("achievement_points", 0) - self.world = world - self.former_world = kwargs.get("former_world") - self.residence = kwargs.get("residence") - self.married_to = kwargs.get("married_to") + self.level = level # type: int + self.achievement_points = kwargs.get("achievement_points", 0) # type: int + self.world = world # type: str + self.former_world = kwargs.get("former_world") # type: Optional[str] + self.residence = kwargs.get("residence") # type: str + self.married_to = kwargs.get("married_to") # type: Optional[str] self.house = kwargs.get("house") # type: Optional[CharacterHouse] self.guild_membership = kwargs.get("guild_membership") # type: Optional[GuildMembership] self.last_login = try_datetime(kwargs.get("last_login")) self.account_status = try_enum(AccountStatus, kwargs.get("account_status")) - self.position = try_enum(AccountStatus, kwargs.get("account_status")) - self.position = kwargs.get("position") - self.comment = kwargs.get("comment") + self.position = kwargs.get("position") # type: Optional[str] + self.comment = kwargs.get("comment") # type: Optional[str] self.achievements = kwargs.get("achievements", []) # type: List[Achievement] self.deaths = kwargs.get("deaths", []) # type: List[Death] self.account_information = kwargs.get("account_information") # type: Optional[AccountInformation] diff --git a/tibiapy/enums.py b/tibiapy/enums.py index 4c02bbd3..d90acf05 100644 --- a/tibiapy/enums.py +++ b/tibiapy/enums.py @@ -16,6 +16,7 @@ class AccountStatus(BaseEnum): class Category(BaseEnum): + """The different highscores categories.""" ACHIEVEMENTS = "achievements" AXE_FIGHTING = "axe" CLUB_FIGHTING = "club" @@ -78,6 +79,7 @@ class Vocation(BaseEnum): class VocationFilter(Enum): + """The vocation filters available for Highscores.""" ALL = 0 KNIGHTS = 1 PALADINS = 2 diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py index 1b7ab810..050fe007 100644 --- a/tibiapy/highscores.py +++ b/tibiapy/highscores.py @@ -1,25 +1,91 @@ import re from collections import OrderedDict +from typing import List -from tibiapy import abc, InvalidContent, VocationFilter, Category, Vocation +from tibiapy import Category, InvalidContent, Vocation, VocationFilter, abc from tibiapy.utils import parse_tibiacom_content, try_enum -__all__ = ("Highscores", "HighscoresEntry") +__all__ = ("ExpHighscoresEntry", "Highscores", "HighscoresEntry", "LoyaltyHighscoresEntry") results_pattern = re.compile(r'Results: (\d+)') +HIGHSCORES_URL = "https://secure.tibia.com/community/?subtopic=highscores&world=%s&list=%s&profession=%d¤tpage=%d" class Highscores(abc.Serializable): + """Represents the highscores of a world. + + Tibia.com only shows 25 entries per page. + TibiaData.com shows all results at once. + + Attributes + ---------- + world: :class:`world` + The world the highscores belong to. + category: :class:`Category` + The selected category to displays the highscores of. + vocation: :class:`VocationFilter` + The selected vocation to filter out values. + results_count: :class:`int` + The total amount of highscores entries in this category. These may be shown in another page. + """ def __init__(self, world, category, **kwargs): - self.world = world - self.category = try_enum(Category, category) + self.world = world # type: str + self.category = try_enum(Category, category, Category.EXPERIENCE) self.vocation = try_enum(VocationFilter, kwargs.get("vocation"), VocationFilter.ALL) - self.entries = kwargs.get("entries", []) - self.results_count = kwargs.get("results_count") + self.entries = kwargs.get("entries", []) # type: List[HighscoresEntry] + self.results_count = kwargs.get("results_count") # type: int + + def __repr__(self) -> str: + return "<{0.__class__.__name__} world={0.world!r} category={0.category!r} vocation={0.vocation!r}>".format(self) + + @property + def from_rank(self): + """:class:`int`: The starting rank of the provided entries.""" + return self.entries[0].rank if self.entries else 0 + + @property + def to_rank(self): + """:class:`int`: The last rank of the provided entries.""" + return self.entries[-1].rank if self.entries else 0 + @property + def page(self): + """:class:`int`: The page number the shown results correspond to on Tibia.com""" + return int(self.from_rank/25)+1 if self.from_rank else 0 + + @property + def total_pages(self): + """:class:`int`: The total of pages of the highscores category.""" + return int(self.results_count/25) + + @property + def url(self): + """:class:`str`: The URL to the highscores page on Tibia.com containing the results.""" + return self.get_url(self.world, self.category, self.vocation, self.page) @classmethod def from_content(cls, content): + """Creates an instance of the class from the html content of a highscores page. + + Notes + ----- + Tibia.com only shows up to 25 entries per page, so in order to obtain the full highscores, all 12 pages must + be parsed and merged into one. + + Parameters + ---------- + content: :class:`str` + The HTML content of the page. + + Returns + ------- + :class:`Highscores` + The highscores results contained in the page. + + Raises + ------ + InvalidContent + If content is not the HTML of a highscore's page.""" parsed_content = parse_tibiacom_content(content) tables = cls._parse_tables(parsed_content) filters = tables.get("Highscores Filter") @@ -27,8 +93,11 @@ def from_content(cls, content): raise InvalidContent() world_filter, vocation_filter, category_filter = filters world = world_filter.find("option", {"selected": True})["value"] - vocation = int(vocation_filter.find("option", {"selected": True})["value"]) + if world == "": + return None category = category_filter.find("option", {"selected": True})["value"] + vocation_selected = vocation_filter.find("option", {"selected": True}) + vocation = int(vocation_selected["value"]) if vocation_selected else 0 highscores = cls(world, category, vocation=vocation) entries = tables.get("Highscores") if entries is None: @@ -38,15 +107,34 @@ def from_content(cls, content): highscores.results_count = int(results_pattern.search(info_row.text).group(1)) for row in rows: cols_raw = row.find_all('td') - cols_clean = [c.text.replace('\xa0', ' ').strip() for c in cols_raw] - highscores.parse_entry(cols_clean) + highscores._parse_entry(cols_raw) return highscores + @classmethod + def get_url(cls, world, category=Category.EXPERIENCE, vocation=VocationFilter.ALL, page=1): + """Gets the Tibia.com URL of the highscores for the given parameters. + + Parameters + ---------- + world: :class:`str` + The game world of the desired highscores. + category: :class:`Category` + The desired highscores category. + vocation: :class:`VocationFiler` + The vocation filter to apply. By default all vocations will be shown. + page: :class:`int` + The page of highscores to show. + + Returns + ------- + The URL to the Tibia.com highscores. + """ + return HIGHSCORES_URL % (world, category.value, vocation.value, page) @classmethod def _parse_tables(cls, parsed_content): """ - Parses the information tables found in a world's information page. + Parses the information tables found in a highscores page. Parameters ---------- @@ -68,21 +156,88 @@ def _parse_tables(cls, parsed_content): return output # endregion - def parse_entry(self, cols_clean): - rank, name, vocation, *values = cols_clean + def _parse_entry(self, cols): + """Parses an entry's row and adds the result to py:attr:`entries`. + + Parameters + ---------- + cols: :class:`bs4.ResultSet` + The list of columns for that entry. + """ + rank, name, vocation, *values = [c.text.replace('\xa0', ' ').strip() for c in cols] rank = int(rank) if self.category == Category.EXPERIENCE or self.category == Category.LOYALTY_POINTS: - _, value = values + extra, value = values else: - value, *_ = values + value, *extra = values value = int(value.replace(',', '')) - entry = HighscoresEntry(name, rank, vocation, value) + if self.category == Category.EXPERIENCE: + entry = ExpHighscoresEntry(name, rank, vocation, value, int(extra)) + elif self.category == Category.LOYALTY_POINTS: + entry = LoyaltyHighscoresEntry(name, rank, vocation, value, extra) + else: + entry = HighscoresEntry(name, rank, vocation, value) self.entries.append(entry) class HighscoresEntry(abc.BaseCharacter): + """Represents a entry for the highscores. + + Attributes + ---------- + name: :class:`str` + The name of the character. + rank: :class:`int` + The character's rank in the respective highscores. + vocation: :class:`Vocation` + The character's vocation. + value: :class:`int` + The character's value for the highscores.""" def __init__(self, name, rank, vocation, value): self.name = name self.rank = rank self.vocation = try_enum(Vocation, vocation) self.value = value + + def __repr__(self) -> str: + return "<{0.__class__.__name__} rank={0.rank} name={0.name!r} value={0.value}>".format(self) + + +class ExpHighscoresEntry(HighscoresEntry): + """Represents a entry for the highscores's experience category. + + Attributes + ---------- + name: :class:`str` + The name of the character. + rank: :class:`int` + The character's rank in the respective highscores. + vocation: :class:`Vocation` + The character's vocation. + value: :class:`int` + The character's experience points. + level: :class:`int` + The character's level.""" + def __init__(self, name, rank, vocation, value, level): + super().__init__(name, rank, vocation, value) + self.level = level + + +class LoyaltyHighscoresEntry(HighscoresEntry): + """Represents a entry for the highscores loyalty points category. + + Attributes + ---------- + name: :class:`str` + The name of the character. + rank: :class:`int` + The character's rank in the respective highscores. + vocation: :class:`Vocation` + The character's vocation. + value: :class:`int` + The character's loyalty points. + title: :class:`str` + The character's loyalty title.""" + def __init__(self, name, rank, vocation, value, title): + super().__init__(name, rank, vocation, value) + self.title = title diff --git a/tibiapy/utils.py b/tibiapy/utils.py index 86688b11..8581173d 100644 --- a/tibiapy/utils.py +++ b/tibiapy/utils.py @@ -190,7 +190,7 @@ def try_datetime(obj) -> Optional[datetime.datetime]: Returns ------- - :class:`datetime.datetime` + :class:`datetime.datetime`, optional The represented datetime, or ``None`` if conversion wasn't possible. """ if obj is None: @@ -277,7 +277,7 @@ def try_enum(cls: Type[T], val, default: D = None) -> Union[T, D]: Returns ------- - any + obj: The enum value if found, otherwise None. """ if isinstance(val, cls): From 85409665444d1344dd2d9fc7144ca214773ed713 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 7 Jan 2019 15:01:16 -0700 Subject: [PATCH 05/12] Change in travis CI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ae0d31cc..e1dc9770 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,11 +25,11 @@ script: - make html - cd .. -after_sucess: +after_script: - coverage report - coverage xml - - codecov - if [[ "$TRAVIS_PULL_REQUEST" == "false" && "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi + - if [[ "$TRAVIS_PULL_REQUEST" == "false" && "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then codecov; fi deploy: provider: pages From 7b43a3135fb4e4868cd46cbcddfd943681d4bdc2 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 7 Jan 2019 17:41:11 -0700 Subject: [PATCH 06/12] Implemented TibiaData highscores parsing - Added TibiaData test cases - Added recursive trim function for json parsing --- tests/resources/highscores/tibiacom_full.txt | 10 +- .../resources/highscores/tibiadata_empty.json | 1 + .../highscores/tibiadata_experience.json | 1 + .../resources/highscores/tibiadata_full.json | 1 + .../highscores/tibiadata_loyalty.json | 1 + tests/tests_highscores.py | 89 +++++++++++++++++- tests/tests_house.py | 38 ++++---- tibiapy/character.py | 10 +- tibiapy/guild.py | 16 +--- tibiapy/highscores.py | 92 ++++++++++++++++++- tibiapy/house.py | 15 +-- tibiapy/utils.py | 39 ++++++++ tibiapy/world.py | 15 +-- 13 files changed, 252 insertions(+), 76 deletions(-) create mode 100644 tests/resources/highscores/tibiadata_empty.json create mode 100644 tests/resources/highscores/tibiadata_experience.json create mode 100644 tests/resources/highscores/tibiadata_full.json create mode 100644 tests/resources/highscores/tibiadata_loyalty.json diff --git a/tests/resources/highscores/tibiacom_full.txt b/tests/resources/highscores/tibiacom_full.txt index e10b7dd1..ac2538e6 100644 --- a/tests/resources/highscores/tibiacom_full.txt +++ b/tests/resources/highscores/tibiacom_full.txt @@ -1,8 +1,4 @@ -
-
Highscores Filter
World:
Vocation:
Category:

Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).

Highscores
RankNameVocationLevel
1SkogisElite Knight13
2Sebbe Da NukerElite Knight12
3Dark AmmbaElite Knight12
4ExorinhoElite Knight12
5WivivolElite Knight12
6Dekos WakuseiElite Knight12
7GerkulesElite Knight12
8Kitty MorganaElite Knight12
9Sir IgoroElite Knight12
10Bomin KenilElite Knight12
11EndrianoKnight12
12Pappa RockyElite Knight12
13RamboartosElite Knight12
14QarmaElite Knight11
15Rezuv TankerKnight11
16Aragorn ElessarKnight11
17Zlaten IbrahimovicKnight11
18Dancer ClubElite Knight11
19Nameck KnightKnight11
20LuthreckElite Knight11
21AvilazinhoKnight11
22AerpionKnight11
23Jeffrey KintaElite Knight11
24Enyx DeathweaverElite Knight11
25Lucky PujjhardElite Knight11
» Pages: 1 2 3 4 5 6 7 8 9 10 11 12
» Results: 300
+
+
Highscores Filter
World:
Vocation:
Category:

Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).

Highscores
RankNameVocationLevel
76Przeokrutny KnightElite Knight11
77Mighty BorisKnight11
78DiasterusElite Knight11
79Werdix LegendElite Knight11
80Pato KinguElite Knight11
81Swetty BellaElite Knight11
82RyllandKnight11
83TazeedKnight11
84Hobab Da TankerElite Knight11
85Ariku KendElite Knight11
86BellegarrElite Knight11
87OdynieckElite Knight11
88XeniaElite Knight11
89KabelganoElite Knight11
90Elite HamattoElite Knight11
91LosoKnight11
92Steel rastalElite Knight11
93AktarielKnight11
94Soviet QueenKnight11
95Lidera OlaaElite Knight11
96AxmenaElite Knight11
97Goblin EonElite Knight11
98Vitao vidalokaElite Knight11
99Raphael MasterfulElite Knight11
100BoolecElite Knight11
» Pages: 1 2 3 4 5 6 7 8 9 10 11 12
» Results: 300
- -
-
-
- \ No newline at end of file + \ No newline at end of file diff --git a/tests/resources/highscores/tibiadata_empty.json b/tests/resources/highscores/tibiadata_empty.json new file mode 100644 index 00000000..3416feb3 --- /dev/null +++ b/tests/resources/highscores/tibiadata_empty.json @@ -0,0 +1 @@ +{"highscores":{"world":"Adaaf","type":"loyalty","data":{"error":"World does not exist."}},"information":{"api_version":2,"execution_time":0.0347,"last_updated":"2019-01-08 00:29:34","timestamp":"2019-01-08 00:29:34"}} \ No newline at end of file diff --git a/tests/resources/highscores/tibiadata_experience.json b/tests/resources/highscores/tibiadata_experience.json new file mode 100644 index 00000000..92b4a6b2 --- /dev/null +++ b/tests/resources/highscores/tibiadata_experience.json @@ -0,0 +1 @@ +{"highscores":{"world":"Luminera","type":"experience","data":[{"name":"Kharsek","rank":1,"voc":"Elite Knight","points":31598234574,"level":1239},{"name":"Kimz Corleone","rank":2,"voc":"Elite Knight","points":17558906223,"level":1019},{"name":"Hecks Leads","rank":3,"voc":"Elder Druid","points":16229044110,"level":993},{"name":"Blessed Duken","rank":4,"voc":"Master Sorcerer","points":14818144659,"level":963},{"name":"Demon Lance","rank":5,"voc":"Royal Paladin","points":13395275344,"level":931},{"name":"Renato Higa","rank":6,"voc":"Royal Paladin","points":10614559348,"level":862},{"name":"Mystrucath","rank":7,"voc":"Elite Knight","points":9993987814,"level":845},{"name":"Pilon Corleone","rank":8,"voc":"Royal Paladin","points":9402278066,"level":828},{"name":"Ferxo Nekro","rank":9,"voc":"Master Sorcerer","points":9051765749,"level":817},{"name":"Wares el mago","rank":10,"voc":"Master Sorcerer","points":8868924285,"level":812},{"name":"Renaszika","rank":11,"voc":"Royal Paladin","points":8769480340,"level":809},{"name":"Tenente Nunez","rank":12,"voc":"Elite Knight","points":8630420635,"level":805},{"name":"Darth Yoshi","rank":13,"voc":"Elite Knight","points":7821094964,"level":779},{"name":"Feliicity","rank":14,"voc":"Elder Druid","points":7186570205,"level":757},{"name":"Delibal","rank":15,"voc":"Royal Paladin","points":6956109677,"level":749},{"name":"Ell Condor","rank":16,"voc":"Elder Druid","points":6732006529,"level":741},{"name":"Lethal Frost","rank":17,"voc":"Elder Druid","points":6295520369,"level":724},{"name":"Ikqer","rank":18,"voc":"Elder Druid","points":6248607502,"level":723},{"name":"Nith The Conqueror","rank":19,"voc":"Elite Knight","points":6082952388,"level":716},{"name":"Silo Baris","rank":20,"voc":"Elite Knight","points":5915022794,"level":710},{"name":"Kratoz Inferno","rank":21,"voc":"Royal Paladin","points":5877406369,"level":708},{"name":"Animalist","rank":22,"voc":"Elite Knight","points":5670300125,"level":700},{"name":"Dragonxi","rank":23,"voc":"Elder Druid","points":5523172568,"level":694},{"name":"Morenao","rank":24,"voc":"Elite Knight","points":5464859926,"level":691},{"name":"Takkun","rank":25,"voc":"Elite Knight","points":5267396351,"level":683},{"name":"Ares Corleone","rank":26,"voc":"Elite Knight","points":5177838591,"level":679},{"name":"Anna Sinixa","rank":27,"voc":"Elder Druid","points":5139195351,"level":677},{"name":"Saint Corleone","rank":28,"voc":"Elder Druid","points":5049023490,"level":673},{"name":"Cachulo","rank":29,"voc":"Elder Druid","points":5041984902,"level":673},{"name":"Demyss Lord","rank":30,"voc":"Master Sorcerer","points":4904292072,"level":667},{"name":"Kiimber","rank":31,"voc":"Elite Knight","points":4902016219,"level":667},{"name":"Kiraxs","rank":32,"voc":"Royal Paladin","points":4891080297,"level":666},{"name":"Mitzora","rank":33,"voc":"Royal Paladin","points":4773116048,"level":661},{"name":"Blessed Betao","rank":34,"voc":"Elder Druid","points":4767506139,"level":660},{"name":"Kenedy Zoria","rank":35,"voc":"Elder Druid","points":4519235695,"level":649},{"name":"Don Jonz","rank":36,"voc":"Elite Knight","points":4390546539,"level":643},{"name":"Kriipty","rank":37,"voc":"Elite Knight","points":4329047827,"level":640},{"name":"Sev Dumbledore","rank":38,"voc":"Elder Druid","points":4308247260,"level":639},{"name":"Sir Philipe","rank":39,"voc":"Elite Knight","points":4112681108,"level":629},{"name":"Quil Silverearth","rank":40,"voc":"Druid","points":4022052696,"level":624},{"name":"Ken Yiro","rank":41,"voc":"Elite Knight","points":3962391687,"level":621},{"name":"Skroll","rank":42,"voc":"Knight","points":3949293108,"level":620},{"name":"Alleeh","rank":43,"voc":"Elite Knight","points":3730415330,"level":609},{"name":"Phanyzhu","rank":44,"voc":"Master Sorcerer","points":3653366023,"level":604},{"name":"Acapellah","rank":45,"voc":"Master Sorcerer","points":3578275977,"level":600},{"name":"Zarcone","rank":46,"voc":"Elite Knight","points":3569655728,"level":600},{"name":"Thray","rank":47,"voc":"Elder Druid","points":3539986608,"level":598},{"name":"Dekain","rank":48,"voc":"Elite Knight","points":3243303375,"level":581},{"name":"Doctrix","rank":49,"voc":"Elder Druid","points":3140706806,"level":575},{"name":"Hallszin","rank":50,"voc":"Elite Knight","points":3130262658,"level":574},{"name":"Nithka","rank":51,"voc":"Elite Knight","points":3068654395,"level":570},{"name":"Othoz Senpai","rank":52,"voc":"Master Sorcerer","points":3062165097,"level":570},{"name":"Sahumerio","rank":53,"voc":"Druid","points":3053178078,"level":569},{"name":"Anth Lelf raid","rank":54,"voc":"Elite Knight","points":2994658156,"level":566},{"name":"Awkward Discharge","rank":55,"voc":"Master Sorcerer","points":2964664229,"level":564},{"name":"Coro Nel","rank":56,"voc":"Elite Knight","points":2872686744,"level":558},{"name":"Kendrickzs","rank":57,"voc":"Elite Knight","points":2837883432,"level":556},{"name":"Farapus","rank":58,"voc":"Elite Knight","points":2802736650,"level":553},{"name":"Hans Boa","rank":59,"voc":"Master Sorcerer","points":2752592524,"level":550},{"name":"Deanth Tris","rank":60,"voc":"Knight","points":2744653793,"level":550},{"name":"Amilla","rank":61,"voc":"Elite Knight","points":2669167310,"level":545},{"name":"Bapumel","rank":62,"voc":"Druid","points":2659030895,"level":544},{"name":"Dekoin","rank":63,"voc":"Elder Druid","points":2618585357,"level":541},{"name":"Tifon","rank":64,"voc":"Master Sorcerer","points":2566334736,"level":537},{"name":"Aragons Corleone","rank":65,"voc":"Elite Knight","points":2544839768,"level":536},{"name":"Jafzz","rank":66,"voc":"Elder Druid","points":2539872411,"level":536},{"name":"Milkz Archangel","rank":67,"voc":"Royal Paladin","points":2462677500,"level":530},{"name":"Dagroth Strax","rank":68,"voc":"Elder Druid","points":2452496587,"level":529},{"name":"Don Pablo Ruan De La Vega","rank":69,"voc":"Elite Knight","points":2418658749,"level":527},{"name":"Donn Cloudy","rank":70,"voc":"Elder Druid","points":2389713250,"level":525},{"name":"Tita Dlos Angeles","rank":71,"voc":"Sorcerer","points":2373512166,"level":524},{"name":"Hayllannder","rank":72,"voc":"Elite Knight","points":2358251149,"level":523},{"name":"Deley Arcano","rank":73,"voc":"Sorcerer","points":2339738405,"level":521},{"name":"Lenuberius","rank":74,"voc":"Elder Druid","points":2294198025,"level":518},{"name":"Kelzinha","rank":75,"voc":"Elite Knight","points":2246453350,"level":514},{"name":"General Madness","rank":76,"voc":"Royal Paladin","points":2237425738,"level":514},{"name":"Druid Rhider","rank":77,"voc":"Druid","points":2106041012,"level":503},{"name":"Hazard Magic","rank":78,"voc":"Elite Knight","points":2084019119,"level":502},{"name":"Dant Ivan","rank":79,"voc":"Elite Knight","points":2073018231,"level":501},{"name":"Drapskin","rank":80,"voc":"Master Sorcerer","points":2045986693,"level":498},{"name":"Krotarek","rank":81,"voc":"Royal Paladin","points":2042454042,"level":498},{"name":"Cona Larsonti","rank":82,"voc":"Knight","points":1998756238,"level":495},{"name":"Don Will","rank":83,"voc":"Elder Druid","points":1988639767,"level":494},{"name":"Jaquelynne","rank":84,"voc":"Elder Druid","points":1980112685,"level":493},{"name":"Killer Uri","rank":85,"voc":"Royal Paladin","points":1976696773,"level":493},{"name":"Shooting Blanks","rank":86,"voc":"Royal Paladin","points":1966023531,"level":492},{"name":"Insane Dean","rank":87,"voc":"Elder Druid","points":1962594953,"level":492},{"name":"Thee angelina","rank":88,"voc":"Paladin","points":1955673689,"level":491},{"name":"Hyudi","rank":89,"voc":"Elite Knight","points":1922179265,"level":488},{"name":"Dudaw","rank":90,"voc":"Knight","points":1905295228,"level":487},{"name":"Upsett Mind","rank":91,"voc":"Elite Knight","points":1903470121,"level":487},{"name":"Shane Guin","rank":92,"voc":"Royal Paladin","points":1889245507,"level":485},{"name":"Blazing Hard","rank":93,"voc":"Elite Knight","points":1876001931,"level":484},{"name":"Slothfulness","rank":94,"voc":"Master Sorcerer","points":1840697264,"level":481},{"name":"Ruminahui Knight","rank":95,"voc":"Elite Knight","points":1839136051,"level":481},{"name":"Letal Corleone","rank":96,"voc":"Master Sorcerer","points":1837135113,"level":481},{"name":"Upier","rank":97,"voc":"Elite Knight","points":1790287440,"level":477},{"name":"Pappie","rank":98,"voc":"Elite Knight","points":1785286493,"level":476},{"name":"Lord Philippe","rank":99,"voc":"Knight","points":1775165653,"level":476},{"name":"Sir Darknees","rank":100,"voc":"Elite Knight","points":1775059420,"level":476},{"name":"Desire Divine","rank":101,"voc":"Druid","points":1764636397,"level":475},{"name":"Caxote","rank":102,"voc":"Knight","points":1756899244,"level":474},{"name":"Drako ox","rank":103,"voc":"Elder Druid","points":1751255618,"level":473},{"name":"Renyod Nith","rank":104,"voc":"Royal Paladin","points":1726116056,"level":471},{"name":"Cachudo","rank":105,"voc":"Elite Knight","points":1703827465,"level":469},{"name":"Milteki","rank":106,"voc":"Elite Knight","points":1690659740,"level":468},{"name":"Shirohigue","rank":107,"voc":"Elder Druid","points":1683907295,"level":467},{"name":"Sir Faker Loiro","rank":108,"voc":"Sorcerer","points":1660064678,"level":465},{"name":"Hunson Abadir","rank":109,"voc":"Royal Paladin","points":1658194532,"level":465},{"name":"Estelar","rank":110,"voc":"Paladin","points":1643761237,"level":464},{"name":"Lord Lelouch","rank":111,"voc":"Elder Druid","points":1639660406,"level":463},{"name":"Pow-rep","rank":112,"voc":"Royal Paladin","points":1617270426,"level":461},{"name":"Geno Castillo","rank":113,"voc":"Elite Knight","points":1605057294,"level":460},{"name":"Dottero","rank":114,"voc":"Elite Knight","points":1601247848,"level":460},{"name":"Thordil Clawu","rank":115,"voc":"Sorcerer","points":1593258338,"level":459},{"name":"Skiann Torphyx","rank":116,"voc":"Elite Knight","points":1577351199,"level":457},{"name":"Thanos Haardcore","rank":117,"voc":"Elite Knight","points":1568638639,"level":456},{"name":"Hazzus","rank":118,"voc":"Royal Paladin","points":1567699319,"level":456},{"name":"Nofkyzx","rank":119,"voc":"Royal Paladin","points":1562079766,"level":456},{"name":"Victorpitta","rank":120,"voc":"Elite Knight","points":1561542133,"level":456},{"name":"Polaris Hell","rank":121,"voc":"Paladin","points":1549369247,"level":455},{"name":"Smitty Werbenjager Manjenson","rank":122,"voc":"Master Sorcerer","points":1538772446,"level":453},{"name":"Dexteer Morgan","rank":123,"voc":"Elder Druid","points":1532027223,"level":453},{"name":"Constantin von Carstein","rank":124,"voc":"Druid","points":1521772274,"level":452},{"name":"Wells Pargu","rank":125,"voc":"Elite Knight","points":1514622174,"level":451},{"name":"Future Jack","rank":126,"voc":"Master Sorcerer","points":1503528363,"level":450},{"name":"Insane Potter Dashow","rank":127,"voc":"Master Sorcerer","points":1481311368,"level":448},{"name":"Galluzera","rank":128,"voc":"Knight","points":1465107602,"level":446},{"name":"Efrain Barraza","rank":129,"voc":"Elder Druid","points":1464358573,"level":446},{"name":"Glan Dragonheart","rank":130,"voc":"Elite Knight","points":1461716858,"level":446},{"name":"Felipe Guedes","rank":131,"voc":"Elite Knight","points":1457155891,"level":445},{"name":"Yenlowang","rank":132,"voc":"Master Sorcerer","points":1452673974,"level":445},{"name":"Milez Corleone","rank":133,"voc":"Royal Paladin","points":1442825262,"level":444},{"name":"Insane Groove","rank":134,"voc":"Sorcerer","points":1429324881,"level":442},{"name":"Guilha","rank":135,"voc":"Knight","points":1422315464,"level":442},{"name":"Quan Zhi","rank":136,"voc":"Elite Knight","points":1416007285,"level":441},{"name":"Klussunia","rank":137,"voc":"Knight","points":1410118081,"level":441},{"name":"Tiifon","rank":138,"voc":"Elite Knight","points":1407915024,"level":440},{"name":"Smy","rank":139,"voc":"Elder Druid","points":1407644548,"level":440},{"name":"Kimber'Hearth","rank":140,"voc":"Knight","points":1387585783,"level":438},{"name":"Paladin Iohann","rank":141,"voc":"Royal Paladin","points":1362669415,"level":436},{"name":"Abadon Corleone","rank":142,"voc":"Knight","points":1362653808,"level":436},{"name":"Principito Yartusin","rank":143,"voc":"Elite Knight","points":1357612385,"level":435},{"name":"Skoa","rank":144,"voc":"Knight","points":1335731106,"level":433},{"name":"Silent Sio","rank":145,"voc":"Druid","points":1317854154,"level":431},{"name":"Docinha Amoroza","rank":146,"voc":"Elite Knight","points":1311777008,"level":430},{"name":"Macky darknes","rank":147,"voc":"Elite Knight","points":1306679443,"level":429},{"name":"Lia Liannon","rank":148,"voc":"Druid","points":1305425717,"level":429},{"name":"Pizzuh","rank":149,"voc":"Royal Paladin","points":1297641501,"level":429},{"name":"Michaelmarc","rank":150,"voc":"Royal Paladin","points":1294924074,"level":428},{"name":"Hagorky","rank":151,"voc":"Elite Knight","points":1292842226,"level":428},{"name":"Kughv","rank":152,"voc":"Master Sorcerer","points":1290349409,"level":428},{"name":"Lagarto knight","rank":153,"voc":"Knight","points":1289217649,"level":428},{"name":"Roddars","rank":154,"voc":"Elite Knight","points":1289052827,"level":428},{"name":"Mangoleo","rank":155,"voc":"Elder Druid","points":1287553953,"level":427},{"name":"Kydok","rank":156,"voc":"Elder Druid","points":1280858503,"level":427},{"name":"Zeus el Protector","rank":157,"voc":"Elder Druid","points":1263151471,"level":425},{"name":"Edinho","rank":158,"voc":"Royal Paladin","points":1262798334,"level":425},{"name":"Siegfried blooded knight","rank":159,"voc":"Elite Knight","points":1260808899,"level":424},{"name":"Doctor Gato","rank":160,"voc":"Elite Knight","points":1259433383,"level":424},{"name":"Deviush","rank":161,"voc":"Druid","points":1258622673,"level":424},{"name":"Acid Poth","rank":162,"voc":"Sorcerer","points":1256045328,"level":424},{"name":"Fiona green","rank":163,"voc":"Elite Knight","points":1227286259,"level":421},{"name":"Drofh","rank":164,"voc":"Sorcerer","points":1226757997,"level":421},{"name":"Eibo","rank":165,"voc":"Elite Knight","points":1217948989,"level":420},{"name":"Danili","rank":166,"voc":"Elder Druid","points":1214597792,"level":419},{"name":"Be Joni","rank":167,"voc":"Royal Paladin","points":1204569058,"level":418},{"name":"Maltz","rank":168,"voc":"Sorcerer","points":1202793555,"level":418},{"name":"Celtic Spirit","rank":169,"voc":"Master Sorcerer","points":1198457335,"level":417},{"name":"Presidente catra","rank":170,"voc":"Elder Druid","points":1196894384,"level":417},{"name":"Luca Star","rank":171,"voc":"Elite Knight","points":1195055906,"level":417},{"name":"Kelphor","rank":172,"voc":"Sorcerer","points":1188230900,"level":416},{"name":"Imperatore Skywalker","rank":173,"voc":"Knight","points":1179876374,"level":415},{"name":"Shadows Dani","rank":174,"voc":"Elder Druid","points":1169345780,"level":414},{"name":"Utribull","rank":175,"voc":"Elite Knight","points":1166986286,"level":414},{"name":"Rawrthur","rank":176,"voc":"Elite Knight","points":1165959332,"level":414},{"name":"Endzu","rank":177,"voc":"Elder Druid","points":1159713943,"level":413},{"name":"Cobra Namastech","rank":178,"voc":"Elder Druid","points":1156321459,"level":412},{"name":"Roma Ston","rank":179,"voc":"Elite Knight","points":1154959745,"level":412},{"name":"Dutty'psy","rank":180,"voc":"Elite Knight","points":1151179660,"level":412},{"name":"Efestio","rank":181,"voc":"Elder Druid","points":1150326468,"level":412},{"name":"Katzwinckel","rank":182,"voc":"Elder Druid","points":1144993875,"level":411},{"name":"Lagarto Druid","rank":183,"voc":"Druid","points":1144635383,"level":411},{"name":"Be Lukas","rank":184,"voc":"Elite Knight","points":1142266275,"level":411},{"name":"Crowk vition","rank":185,"voc":"Elite Knight","points":1141935012,"level":411},{"name":"Shrek Constantine","rank":186,"voc":"Elite Knight","points":1140729922,"level":411},{"name":"Dandark","rank":187,"voc":"Elite Knight","points":1137220963,"level":410},{"name":"Horic Seph","rank":188,"voc":"Sorcerer","points":1131580137,"level":409},{"name":"Black Baboon","rank":189,"voc":"Master Sorcerer","points":1128823162,"level":409},{"name":"Usirex Leads","rank":190,"voc":"Royal Paladin","points":1119238945,"level":408},{"name":"Cymos Amela","rank":191,"voc":"Elite Knight","points":1115954366,"level":408},{"name":"Maener Astrikaia","rank":192,"voc":"Master Sorcerer","points":1115454657,"level":408},{"name":"Flamed'Fox","rank":193,"voc":"Royal Paladin","points":1103212238,"level":406},{"name":"Glacies Lupus","rank":194,"voc":"Elite Knight","points":1102690366,"level":406},{"name":"Tesla Koil","rank":195,"voc":"Elite Knight","points":1102228001,"level":406},{"name":"Andre","rank":196,"voc":"Elder Druid","points":1098393952,"level":405},{"name":"Zowux Nogan","rank":197,"voc":"Royal Paladin","points":1092271787,"level":405},{"name":"Cahzinhuw","rank":198,"voc":"Knight","points":1091806118,"level":405},{"name":"Prokurator Generalny","rank":199,"voc":"Knight","points":1089287421,"level":404},{"name":"Mestre Ranzinza","rank":200,"voc":"Elite Knight","points":1087664886,"level":404},{"name":"Mizzrym Druid","rank":201,"voc":"Elder Druid","points":1083705798,"level":404},{"name":"Princess Stormbringer","rank":202,"voc":"Elite Knight","points":1082811494,"level":404},{"name":"Koxak","rank":203,"voc":"Paladin","points":1081213975,"level":403},{"name":"Uderere","rank":204,"voc":"Elite Knight","points":1078918000,"level":403},{"name":"Awesome Lootting","rank":205,"voc":"Royal Paladin","points":1074889709,"level":403},{"name":"Little Sonic","rank":206,"voc":"Elder Druid","points":1073470417,"level":402},{"name":"Kairikou","rank":207,"voc":"Elite Knight","points":1071290369,"level":402},{"name":"Ladystar","rank":208,"voc":"Knight","points":1070266542,"level":402},{"name":"Andruss","rank":209,"voc":"Paladin","points":1069653639,"level":402},{"name":"Lord Morcegu","rank":210,"voc":"Master Sorcerer","points":1067646403,"level":402},{"name":"Chico Mage","rank":211,"voc":"Master Sorcerer","points":1064348900,"level":401},{"name":"Rahx","rank":212,"voc":"Knight","points":1063424990,"level":401},{"name":"Kaori Sato","rank":213,"voc":"Knight","points":1061636290,"level":401},{"name":"Labellamafiaa","rank":214,"voc":"Royal Paladin","points":1058782816,"level":401},{"name":"Bibitao Deroskinis","rank":215,"voc":"Knight","points":1058720173,"level":401},{"name":"Helequyn","rank":216,"voc":"Sorcerer","points":1056884202,"level":400},{"name":"Legolas'Arch","rank":217,"voc":"Paladin","points":1053952064,"level":400},{"name":"Sir Floydz","rank":218,"voc":"Elite Knight","points":1051582648,"level":400},{"name":"Sird Lordaero","rank":219,"voc":"Knight","points":1048798238,"level":399},{"name":"Luh Thanker","rank":220,"voc":"Elite Knight","points":1030693336,"level":397},{"name":"Tnux","rank":221,"voc":"Knight","points":1024520605,"level":396},{"name":"Furia Socka","rank":222,"voc":"Paladin","points":1022632011,"level":396},{"name":"Mac Kee","rank":223,"voc":"Royal Paladin","points":1015501533,"level":395},{"name":"Choko Corleone","rank":224,"voc":"Master Sorcerer","points":1014856558,"level":395},{"name":"Angerkin","rank":225,"voc":"Paladin","points":1013170340,"level":395},{"name":"Jadh","rank":226,"voc":"Elder Druid","points":1007587663,"level":394},{"name":"Snowlight Shine","rank":227,"voc":"Druid","points":1005489514,"level":394},{"name":"Trussingwolft","rank":228,"voc":"Royal Paladin","points":1003881874,"level":393},{"name":"Fredzera Mais Brabo","rank":229,"voc":"Elder Druid","points":998627644,"level":393},{"name":"Lantri Corleone","rank":230,"voc":"Elder Druid","points":997399790,"level":393},{"name":"Ayami Kojima","rank":231,"voc":"Elite Knight","points":994819860,"level":392},{"name":"Cloud Siphiroth","rank":232,"voc":"Elder Druid","points":992326980,"level":392},{"name":"Gamas Oleb","rank":233,"voc":"Elite Knight","points":991994276,"level":392},{"name":"Kreazzy","rank":234,"voc":"Druid","points":991615608,"level":392},{"name":"Josh-pep","rank":235,"voc":"Royal Paladin","points":990082654,"level":392},{"name":"Manevator","rank":236,"voc":"Elder Druid","points":989134167,"level":392},{"name":"Acid Hawk","rank":237,"voc":"Knight","points":988752962,"level":392},{"name":"Elite Neik","rank":238,"voc":"Elite Knight","points":980283853,"level":390},{"name":"Gasper bad","rank":239,"voc":"Royal Paladin","points":980004606,"level":390},{"name":"Beheader","rank":240,"voc":"Elite Knight","points":979560076,"level":390},{"name":"Brother Druid","rank":241,"voc":"Elder Druid","points":978840857,"level":390},{"name":"Hydemingoo","rank":242,"voc":"Druid","points":978452981,"level":390},{"name":"Elite Zikera","rank":243,"voc":"Elite Knight","points":972360073,"level":389},{"name":"Sektorek","rank":244,"voc":"Elite Knight","points":970529751,"level":389},{"name":"Guuda","rank":245,"voc":"Sorcerer","points":965197410,"level":388},{"name":"Frissa","rank":246,"voc":"Elite Knight","points":964393767,"level":388},{"name":"Chopo Rock","rank":247,"voc":"Knight","points":962286671,"level":388},{"name":"Guardian'Knightt","rank":248,"voc":"Elite Knight","points":956362907,"level":387},{"name":"Killer Kri","rank":249,"voc":"Royal Paladin","points":953270208,"level":387},{"name":"Escorpiana","rank":250,"voc":"Royal Paladin","points":950948291,"level":386},{"name":"Padilha","rank":251,"voc":"Royal Paladin","points":945375407,"level":386},{"name":"Piscolas Druid","rank":252,"voc":"Elder Druid","points":942362811,"level":385},{"name":"Pabluski","rank":253,"voc":"Elite Knight","points":939882343,"level":385},{"name":"Coronel Pharaon","rank":254,"voc":"Sorcerer","points":934269352,"level":384},{"name":"Thekius","rank":255,"voc":"Elite Knight","points":929937067,"level":384},{"name":"Qeeza Detterus","rank":256,"voc":"Royal Paladin","points":928320514,"level":383},{"name":"Titan Szeli","rank":257,"voc":"Royal Paladin","points":927171187,"level":383},{"name":"Nokidake","rank":258,"voc":"Elite Knight","points":921493230,"level":382},{"name":"Masked Lycan","rank":259,"voc":"Elite Knight","points":913263597,"level":381},{"name":"Feral Heal","rank":260,"voc":"Elder Druid","points":913086247,"level":381},{"name":"Avantador","rank":261,"voc":"Royal Paladin","points":908815745,"level":381},{"name":"Erastos","rank":262,"voc":"Paladin","points":908543285,"level":381},{"name":"Manu Corleone","rank":263,"voc":"Elder Druid","points":908518754,"level":381},{"name":"Dakeno","rank":264,"voc":"Knight","points":902475401,"level":380},{"name":"Toph Seyfer","rank":265,"voc":"Royal Paladin","points":889217369,"level":378},{"name":"Jack Niffy","rank":266,"voc":"Elite Knight","points":888683550,"level":378},{"name":"Batatinha Delash","rank":267,"voc":"Royal Paladin","points":888405709,"level":378},{"name":"Lajelaf","rank":268,"voc":"Knight","points":886620964,"level":378},{"name":"Archer Zacha","rank":269,"voc":"Royal Paladin","points":882050469,"level":377},{"name":"Katan Kina","rank":270,"voc":"Elite Knight","points":880760240,"level":377},{"name":"Maly Zlodziej","rank":271,"voc":"Knight","points":880269393,"level":377},{"name":"Broly Pretoriano","rank":272,"voc":"Elite Knight","points":879552990,"level":377},{"name":"Grand Kiing","rank":273,"voc":"Master Sorcerer","points":879058020,"level":377},{"name":"Fury Warlord","rank":274,"voc":"Master Sorcerer","points":875279477,"level":376},{"name":"Paperklip","rank":275,"voc":"Elder Druid","points":872685123,"level":376},{"name":"Kostin","rank":276,"voc":"Elite Knight","points":869723503,"level":375},{"name":"Campeao Mundiall","rank":277,"voc":"Paladin","points":867578264,"level":375},{"name":"Bacana Armado","rank":278,"voc":"Knight","points":866908553,"level":375},{"name":"Tomeczek of Pandoria","rank":279,"voc":"Knight","points":863794536,"level":374},{"name":"Speedblack","rank":280,"voc":"Knight","points":860885645,"level":374},{"name":"Gerin Sorcz","rank":281,"voc":"Master Sorcerer","points":858119088,"level":374},{"name":"Paulla angell","rank":282,"voc":"Royal Paladin","points":852713499,"level":373},{"name":"Goldaor Anzin","rank":283,"voc":"Master Sorcerer","points":851126054,"level":373},{"name":"Traductor","rank":284,"voc":"Royal Paladin","points":850233317,"level":372},{"name":"Vabelias uzcategui","rank":285,"voc":"Elite Knight","points":846734995,"level":372},{"name":"Mysterykid","rank":286,"voc":"Paladin","points":839730906,"level":371},{"name":"Maori Black","rank":287,"voc":"Elite Knight","points":835090301,"level":370},{"name":"Ciloni","rank":288,"voc":"Master Sorcerer","points":833158665,"level":370},{"name":"Thechies","rank":289,"voc":"Sorcerer","points":830347455,"level":369},{"name":"Neftis","rank":290,"voc":"Elite Knight","points":827623413,"level":369},{"name":"Tideron","rank":291,"voc":"Druid","points":827081855,"level":369},{"name":"Japtin Renegade","rank":292,"voc":"Royal Paladin","points":824708376,"level":369},{"name":"Pedro Matias Pedroso","rank":293,"voc":"Elite Knight","points":823901806,"level":369},{"name":"Blink Hunter","rank":294,"voc":"Royal Paladin","points":819157638,"level":368},{"name":"Lich Rei","rank":295,"voc":"Sorcerer","points":819129488,"level":368},{"name":"Eldawinda Aramar","rank":296,"voc":"Master Sorcerer","points":815515701,"level":367},{"name":"King Batagalu","rank":297,"voc":"Druid","points":814254734,"level":367},{"name":"Tankka Hellgorak","rank":298,"voc":"Druid","points":813069975,"level":367},{"name":"Druid Azerrux","rank":299,"voc":"Elder Druid","points":810495141,"level":367},{"name":"Elwin Assin","rank":300,"voc":"Elite Knight","points":809222583,"level":366}]},"information":{"api_version":2,"execution_time":0.1078,"last_updated":"2019-01-07 23:54:40","timestamp":"2019-01-07 23:54:39"}} \ No newline at end of file diff --git a/tests/resources/highscores/tibiadata_full.json b/tests/resources/highscores/tibiadata_full.json new file mode 100644 index 00000000..9a185034 --- /dev/null +++ b/tests/resources/highscores/tibiadata_full.json @@ -0,0 +1 @@ +{"highscores":{"world":"Antica","type":"axe","data":[{"name":"Nemes","rank":1,"voc":"Elite Knight","level":126},{"name":"Fihesute","rank":2,"voc":"Elite Knight","level":125},{"name":"Gustavo of Antica","rank":3,"voc":"Elite Knight","level":123},{"name":"Thorin Steelbeard","rank":4,"voc":"Elite Knight","level":123},{"name":"Nirdor","rank":5,"voc":"Elite Knight","level":123},{"name":"Dryad Darkheart","rank":6,"voc":"Elite Knight","level":123},{"name":"Toxo bravo","rank":7,"voc":"Elite Knight","level":122},{"name":"Kia Brighteye","rank":8,"voc":"Elite Knight","level":122},{"name":"Shion Karasius","rank":9,"voc":"Elite Knight","level":122},{"name":"Mefioo Mclovin","rank":10,"voc":"Elite Knight","level":122},{"name":"Inaga","rank":11,"voc":"Elite Knight","level":122},{"name":"Coyote Wandering Hunter","rank":12,"voc":"Elite Knight","level":122},{"name":"Godsmacked","rank":13,"voc":"Elite Knight","level":122},{"name":"Delpierosaben","rank":14,"voc":"Elite Knight","level":121},{"name":"Skalle p\u00e4r","rank":15,"voc":"Elite Knight","level":121},{"name":"Celtic-Crusader","rank":16,"voc":"Elite Knight","level":121},{"name":"Thorngrim","rank":17,"voc":"Elite Knight","level":121},{"name":"Arngrymn","rank":18,"voc":"Elite Knight","level":121},{"name":"Tuwadi","rank":19,"voc":"Elite Knight","level":120},{"name":"Captin Falcon","rank":20,"voc":"Knight","level":120},{"name":"Kingmare","rank":21,"voc":"Elite Knight","level":120},{"name":"Neo Gear","rank":22,"voc":"Elite Knight","level":120},{"name":"Crypth","rank":23,"voc":"Elite Knight","level":120},{"name":"Ulf Jan","rank":24,"voc":"Elite Knight","level":120},{"name":"Rebelled Knight","rank":25,"voc":"Knight","level":119},{"name":"Faria Highwind","rank":26,"voc":"Knight","level":119},{"name":"Land Stander","rank":27,"voc":"Elite Knight","level":119},{"name":"Devil Orian","rank":28,"voc":"Elite Knight","level":119},{"name":"Travelo","rank":29,"voc":"Elite Knight","level":119},{"name":"Andref","rank":30,"voc":"Elite Knight","level":119},{"name":"Mardin","rank":31,"voc":"Elite Knight","level":119},{"name":"Etus Lenan","rank":32,"voc":"Elite Knight","level":119},{"name":"Silent Solider","rank":33,"voc":"Knight","level":119},{"name":"Kidnay","rank":34,"voc":"Elite Knight","level":119},{"name":"Mysterious knight","rank":35,"voc":"Knight","level":119},{"name":"Vasait","rank":36,"voc":"Elite Knight","level":119},{"name":"Imprator","rank":37,"voc":"Knight","level":119},{"name":"Suave Brigtth","rank":38,"voc":"Knight","level":118},{"name":"Beatbox-Ohlsson","rank":39,"voc":"Elite Knight","level":118},{"name":"Anax Elite","rank":40,"voc":"Elite Knight","level":118},{"name":"Enri Crys","rank":41,"voc":"Elite Knight","level":118},{"name":"Forcee Moberg","rank":42,"voc":"Elite Knight","level":118},{"name":"Kingjambor","rank":43,"voc":"Elite Knight","level":118},{"name":"Angelyyn Darrabban","rank":44,"voc":"Elite Knight","level":118},{"name":"Chakeowena","rank":45,"voc":"Elite Knight","level":118},{"name":"Elajjten","rank":46,"voc":"Elite Knight","level":118},{"name":"Vrabac","rank":47,"voc":"Elite Knight","level":118},{"name":"Antica Biker","rank":48,"voc":"Elite Knight","level":118},{"name":"Hemminki the Traveller","rank":49,"voc":"Elite Knight","level":118},{"name":"Tyran Eka","rank":50,"voc":"Elite Knight","level":118},{"name":"Aramid","rank":51,"voc":"Elite Knight","level":118},{"name":"Walier Abel","rank":52,"voc":"Knight","level":117},{"name":"Juste Bermont","rank":53,"voc":"Elite Knight","level":117},{"name":"Ty ali","rank":54,"voc":"Elite Knight","level":117},{"name":"Cyzar","rank":55,"voc":"Knight","level":117},{"name":"Fortune Arterial","rank":56,"voc":"Elite Knight","level":117},{"name":"Black Falcon","rank":57,"voc":"Elite Knight","level":117},{"name":"Caedmon","rank":58,"voc":"Elite Knight","level":117},{"name":"Archibald Ironfist","rank":59,"voc":"Knight","level":117},{"name":"Wana Oddyn","rank":60,"voc":"Elite Knight","level":117},{"name":"Alexander Bloodblade","rank":61,"voc":"Elite Knight","level":117},{"name":"Warlock of'antica","rank":62,"voc":"Knight","level":117},{"name":"Tuwow","rank":63,"voc":"Elite Knight","level":117},{"name":"Sir blake","rank":64,"voc":"Elite Knight","level":117},{"name":"Rhalgr","rank":65,"voc":"Elite Knight","level":117},{"name":"Twety","rank":66,"voc":"Elite Knight","level":116},{"name":"Falima","rank":67,"voc":"Elite Knight","level":116},{"name":"Blade Swordseeker","rank":68,"voc":"Knight","level":116},{"name":"Meobgood","rank":69,"voc":"Elite Knight","level":116},{"name":"Arely Cao","rank":70,"voc":"Knight","level":116},{"name":"Doedafienden","rank":71,"voc":"Elite Knight","level":116},{"name":"Odin the Fierce","rank":72,"voc":"Elite Knight","level":116},{"name":"Trikxi","rank":73,"voc":"Elite Knight","level":116},{"name":"Skanius Flan","rank":74,"voc":"Knight","level":116},{"name":"Wasapdood","rank":75,"voc":"Elite Knight","level":116},{"name":"Groove Panthe","rank":76,"voc":"Elite Knight","level":116},{"name":"Sephius Aqua","rank":77,"voc":"Knight","level":116},{"name":"Elitarny Zuy","rank":78,"voc":"Knight","level":116},{"name":"Loitandor","rank":79,"voc":"Elite Knight","level":116},{"name":"Vegga Urdal","rank":80,"voc":"Knight","level":116},{"name":"Almighty Osvetnik","rank":81,"voc":"Elite Knight","level":116},{"name":"Sergio Peopleslayer","rank":82,"voc":"Elite Knight","level":116},{"name":"Pyziaa","rank":83,"voc":"Elite Knight","level":115},{"name":"Soul Skiller Antica","rank":84,"voc":"Elite Knight","level":115},{"name":"Psykogodis","rank":85,"voc":"Knight","level":115},{"name":"Flying Vegetable Sausage","rank":86,"voc":"Elite Knight","level":115},{"name":"Bully the Headshooter","rank":87,"voc":"Elite Knight","level":115},{"name":"Elite Maister","rank":88,"voc":"Elite Knight","level":115},{"name":"Flad Loco","rank":89,"voc":"Elite Knight","level":115},{"name":"Luichi Atka","rank":90,"voc":"Elite Knight","level":115},{"name":"Veraxiz","rank":91,"voc":"Elite Knight","level":115},{"name":"Thud","rank":92,"voc":"Knight","level":115},{"name":"Diko Mado","rank":93,"voc":"Knight","level":115},{"name":"Alderbay","rank":94,"voc":"Elite Knight","level":115},{"name":"Lady Oguro","rank":95,"voc":"Elite Knight","level":115},{"name":"Stor Potatis","rank":96,"voc":"Knight","level":115},{"name":"Lyceldin","rank":97,"voc":"Knight","level":115},{"name":"Milivioo","rank":98,"voc":"Elite Knight","level":115},{"name":"Bogi Insane","rank":99,"voc":"Elite Knight","level":115},{"name":"Yhm Szwagier","rank":100,"voc":"Elite Knight","level":115},{"name":"Therathos","rank":101,"voc":"Knight","level":115},{"name":"Maheloas","rank":102,"voc":"Knight","level":114},{"name":"Bass Titan","rank":103,"voc":"Knight","level":114},{"name":"Hento Szo","rank":104,"voc":"Elite Knight","level":114},{"name":"Curtiz Benoit","rank":105,"voc":"Knight","level":114},{"name":"Zeika Lelty","rank":106,"voc":"Elite Knight","level":114},{"name":"Mystic Anticanian","rank":107,"voc":"Elite Knight","level":114},{"name":"Avzkor","rank":108,"voc":"Elite Knight","level":114},{"name":"Bembu","rank":109,"voc":"Elite Knight","level":114},{"name":"Parienda","rank":110,"voc":"Elite Knight","level":114},{"name":"Chino Karanza","rank":111,"voc":"Elite Knight","level":114},{"name":"Bruppo","rank":112,"voc":"Elite Knight","level":114},{"name":"Fire Son","rank":113,"voc":"Elite Knight","level":114},{"name":"Decyludo","rank":114,"voc":"Elite Knight","level":114},{"name":"Dominator Szu","rank":115,"voc":"Elite Knight","level":114},{"name":"Isildoor","rank":116,"voc":"Knight","level":114},{"name":"Milrinona","rank":117,"voc":"Elite Knight","level":114},{"name":"Doug Fux","rank":118,"voc":"Elite Knight","level":114},{"name":"Wulitux","rank":119,"voc":"Knight","level":114},{"name":"Dead Rebel","rank":120,"voc":"Elite Knight","level":114},{"name":"Stamatis Gonidis","rank":121,"voc":"Knight","level":114},{"name":"Tenacitas","rank":122,"voc":"Elite Knight","level":114},{"name":"Fllaash","rank":123,"voc":"Elite Knight","level":114},{"name":"Madziuncia","rank":124,"voc":"Knight","level":114},{"name":"Rampazum","rank":125,"voc":"Knight","level":114},{"name":"Mugen Edo","rank":126,"voc":"Knight","level":114},{"name":"Marta Tarud","rank":127,"voc":"Elite Knight","level":114},{"name":"Broodje Ham","rank":128,"voc":"Elite Knight","level":113},{"name":"Mad Beastmode Dogg","rank":129,"voc":"Elite Knight","level":113},{"name":"Xalifox","rank":130,"voc":"Elite Knight","level":113},{"name":"Theron on Antica","rank":131,"voc":"Elite Knight","level":113},{"name":"Nolanbow","rank":132,"voc":"Elite Knight","level":113},{"name":"Szwendaxus","rank":133,"voc":"Elite Knight","level":113},{"name":"Iron Elveran","rank":134,"voc":"Elite Knight","level":113},{"name":"Linn Chopper","rank":135,"voc":"Elite Knight","level":113},{"name":"Zepius Falyn","rank":136,"voc":"Elite Knight","level":113},{"name":"Skyzz","rank":137,"voc":"Elite Knight","level":113},{"name":"Fear Master","rank":138,"voc":"Elite Knight","level":113},{"name":"Rikon Defender","rank":139,"voc":"Elite Knight","level":113},{"name":"Prospector Number One","rank":140,"voc":"Knight","level":113},{"name":"Soldier Elite","rank":141,"voc":"Elite Knight","level":113},{"name":"Szybki Michal","rank":142,"voc":"Elite Knight","level":113},{"name":"Princess Dema","rank":143,"voc":"Elite Knight","level":113},{"name":"Rayn Sareia","rank":144,"voc":"Elite Knight","level":113},{"name":"Karxago","rank":145,"voc":"Knight","level":113},{"name":"Ozukus","rank":146,"voc":"Elite Knight","level":113},{"name":"Wismy","rank":147,"voc":"Elite Knight","level":113},{"name":"Ablloker","rank":148,"voc":"Elite Knight","level":113},{"name":"Vallih Kherd","rank":149,"voc":"Elite Knight","level":113},{"name":"Castlez","rank":150,"voc":"Elite Knight","level":113},{"name":"Eternal Khan","rank":151,"voc":"Elite Knight","level":113},{"name":"More Stone","rank":152,"voc":"Elite Knight","level":113},{"name":"Daniel der Axefighter","rank":153,"voc":"Knight","level":113},{"name":"Apart Kreck","rank":154,"voc":"Elite Knight","level":113},{"name":"Nimelo","rank":155,"voc":"Elite Knight","level":113},{"name":"Shir Casi","rank":156,"voc":"Elite Knight","level":112},{"name":"Piuroxis Ek","rank":157,"voc":"Elite Knight","level":112},{"name":"Arwin Winddancer","rank":158,"voc":"Elite Knight","level":112},{"name":"Leya Gowa","rank":159,"voc":"Elite Knight","level":112},{"name":"Pes cavus","rank":160,"voc":"Knight","level":112},{"name":"Drak Valor","rank":161,"voc":"Elite Knight","level":112},{"name":"Chucknorris Angry","rank":162,"voc":"Elite Knight","level":112},{"name":"Michal Fiodorowicz","rank":163,"voc":"Elite Knight","level":112},{"name":"Kjellfish","rank":164,"voc":"Knight","level":112},{"name":"Pisteador","rank":165,"voc":"Elite Knight","level":112},{"name":"Atingel","rank":166,"voc":"Elite Knight","level":112},{"name":"Christian Slayer","rank":167,"voc":"Knight","level":112},{"name":"Arien Dark Dragon","rank":168,"voc":"Knight","level":112},{"name":"Wanmor","rank":169,"voc":"Elite Knight","level":112},{"name":"Air lord","rank":170,"voc":"Knight","level":112},{"name":"Lopet","rank":171,"voc":"Elite Knight","level":112},{"name":"Gey Spartan","rank":172,"voc":"Elite Knight","level":112},{"name":"Bright Black","rank":173,"voc":"Knight","level":112},{"name":"Gaphazi Elite","rank":174,"voc":"Elite Knight","level":112},{"name":"Kazzador","rank":175,"voc":"Knight","level":112},{"name":"Serroch","rank":176,"voc":"Knight","level":112},{"name":"Fobisonek","rank":177,"voc":"Knight","level":112},{"name":"Gim","rank":178,"voc":"Elite Knight","level":112},{"name":"Vaia Carallo","rank":179,"voc":"Elite Knight","level":112},{"name":"Balrog of Morguth","rank":180,"voc":"Elite Knight","level":112},{"name":"Jerivia","rank":181,"voc":"Knight","level":112},{"name":"Loud outlaugher","rank":182,"voc":"Knight","level":112},{"name":"Dosarphic on Antica","rank":183,"voc":"Elite Knight","level":112},{"name":"Fantasy Boo","rank":184,"voc":"Elite Knight","level":112},{"name":"Madlin Matr","rank":185,"voc":"Elite Knight","level":112},{"name":"Magiczny Jaskier","rank":186,"voc":"Elite Knight","level":112},{"name":"Willy Banan","rank":187,"voc":"Elite Knight","level":112},{"name":"Niloth","rank":188,"voc":"Elite Knight","level":112},{"name":"Daxa Alus","rank":189,"voc":"Elite Knight","level":112},{"name":"Kronic Heat","rank":190,"voc":"Elite Knight","level":112},{"name":"Gryphis","rank":191,"voc":"Knight","level":112},{"name":"Xonak Ravenwood","rank":192,"voc":"Knight","level":112},{"name":"Herr Kostas","rank":193,"voc":"Elite Knight","level":112},{"name":"King Florus","rank":194,"voc":"Elite Knight","level":111},{"name":"Xaffav","rank":195,"voc":"Knight","level":111},{"name":"Tote Hose","rank":196,"voc":"Knight","level":111},{"name":"Maniek Prezes","rank":197,"voc":"Knight","level":111},{"name":"Anniana","rank":198,"voc":"Elite Knight","level":111},{"name":"Rheraw","rank":199,"voc":"Elite Knight","level":111},{"name":"Argon Fufus","rank":200,"voc":"Elite Knight","level":111},{"name":"Smoerify","rank":201,"voc":"Elite Knight","level":111},{"name":"Greta Med Yxa","rank":202,"voc":"Knight","level":111},{"name":"Wardcome","rank":203,"voc":"Elite Knight","level":111},{"name":"Accelera","rank":204,"voc":"Elite Knight","level":111},{"name":"Mentalbob","rank":205,"voc":"Elite Knight","level":111},{"name":"Wrzut","rank":206,"voc":"Elite Knight","level":111},{"name":"Cres Max","rank":207,"voc":"Knight","level":111},{"name":"Em See Clapyourhands","rank":208,"voc":"Knight","level":111},{"name":"Terror eyes","rank":209,"voc":"Knight","level":111},{"name":"Serpplandia","rank":210,"voc":"Elite Knight","level":111},{"name":"Frodo Baggins","rank":211,"voc":"Elite Knight","level":111},{"name":"Exotic Brah","rank":212,"voc":"Knight","level":111},{"name":"Vigan The Housemaid","rank":213,"voc":"Knight","level":111},{"name":"Dillex","rank":214,"voc":"Elite Knight","level":111},{"name":"Lord Sammael","rank":215,"voc":"Elite Knight","level":111},{"name":"Armina Chan","rank":216,"voc":"Elite Knight","level":111},{"name":"Szynszyl Siedem","rank":217,"voc":"Elite Knight","level":111},{"name":"Sir Darkstars","rank":218,"voc":"Elite Knight","level":111},{"name":"Develoq","rank":219,"voc":"Knight","level":111},{"name":"Kromir","rank":220,"voc":"Elite Knight","level":111},{"name":"Essassa","rank":221,"voc":"Elite Knight","level":111},{"name":"Mustar Ahmed","rank":222,"voc":"Elite Knight","level":111},{"name":"Happa","rank":223,"voc":"Knight","level":111},{"name":"Reequil","rank":224,"voc":"Elite Knight","level":111},{"name":"Ten Tailed Beast","rank":225,"voc":"Elite Knight","level":111},{"name":"Diabluxa","rank":226,"voc":"Elite Knight","level":111},{"name":"Zedbazi","rank":227,"voc":"Elite Knight","level":111},{"name":"Positiveherb","rank":228,"voc":"Knight","level":111},{"name":"Chrysaor Kronos","rank":229,"voc":"Elite Knight","level":111},{"name":"Sir Alpha","rank":230,"voc":"Elite Knight","level":111},{"name":"Nauren","rank":231,"voc":"Elite Knight","level":111},{"name":"Jeeja Yanin","rank":232,"voc":"Elite Knight","level":111},{"name":"Jannos","rank":233,"voc":"Elite Knight","level":111},{"name":"Barbarious","rank":234,"voc":"Elite Knight","level":111},{"name":"Parchat","rank":235,"voc":"Elite Knight","level":111},{"name":"Roshtein","rank":236,"voc":"Elite Knight","level":111},{"name":"Likichina","rank":237,"voc":"Elite Knight","level":110},{"name":"Ijm","rank":238,"voc":"Knight","level":110},{"name":"Ser Hjalmar","rank":239,"voc":"Elite Knight","level":110},{"name":"Army of One","rank":240,"voc":"Elite Knight","level":110},{"name":"Jocke Axx","rank":241,"voc":"Elite Knight","level":110},{"name":"Fa Res","rank":242,"voc":"Elite Knight","level":110},{"name":"Muminas Knight","rank":243,"voc":"Elite Knight","level":110},{"name":"Dodgy Fella","rank":244,"voc":"Elite Knight","level":110},{"name":"Velixy","rank":245,"voc":"Knight","level":110},{"name":"Vereor Nox","rank":246,"voc":"Elite Knight","level":110},{"name":"Vallania Yutha","rank":247,"voc":"Elite Knight","level":110},{"name":"Iketale","rank":248,"voc":"Knight","level":110},{"name":"Xeraphz","rank":249,"voc":"Elite Knight","level":110},{"name":"Lord Nickel","rank":250,"voc":"Knight","level":110},{"name":"Vankisher","rank":251,"voc":"Elite Knight","level":110},{"name":"Draculur","rank":252,"voc":"Knight","level":110},{"name":"Xurryto","rank":253,"voc":"Elite Knight","level":110},{"name":"Projax","rank":254,"voc":"Elite Knight","level":110},{"name":"Sten Bengt","rank":255,"voc":"Knight","level":110},{"name":"Misstafijah","rank":256,"voc":"Elite Knight","level":110},{"name":"Leet","rank":257,"voc":"Elite Knight","level":110},{"name":"Skug odezzy","rank":258,"voc":"Knight","level":110},{"name":"Zorin Oakenshield","rank":259,"voc":"Elite Knight","level":110},{"name":"Maveroth","rank":260,"voc":"Elite Knight","level":110},{"name":"Avarieth","rank":261,"voc":"Elite Knight","level":110},{"name":"Crescit","rank":262,"voc":"Knight","level":110},{"name":"Perry Coxie","rank":263,"voc":"Elite Knight","level":110},{"name":"Soul Female","rank":264,"voc":"Elite Knight","level":110},{"name":"Kiwux","rank":265,"voc":"Elite Knight","level":110},{"name":"Eldarion Taragon","rank":266,"voc":"Elite Knight","level":110},{"name":"Foksh Bentankor","rank":267,"voc":"Elite Knight","level":110},{"name":"Rauppsz Oimerg","rank":268,"voc":"Knight","level":110},{"name":"Darwax","rank":269,"voc":"Elite Knight","level":110},{"name":"Kletex","rank":270,"voc":"Knight","level":110},{"name":"Cymek","rank":271,"voc":"Knight","level":110},{"name":"Under Tanker","rank":272,"voc":"Elite Knight","level":110},{"name":"Primary Suspect","rank":273,"voc":"Knight","level":110},{"name":"Irondust","rank":274,"voc":"Elite Knight","level":110},{"name":"Boss Miszka","rank":275,"voc":"Elite Knight","level":110},{"name":"Eri Jackson","rank":276,"voc":"Elite Knight","level":110},{"name":"Alindox","rank":277,"voc":"Elite Knight","level":110},{"name":"Skopcony Mati","rank":278,"voc":"Elite Knight","level":110},{"name":"Jack Hellforged","rank":279,"voc":"Elite Knight","level":110},{"name":"Massys","rank":280,"voc":"Elite Knight","level":110},{"name":"Nosfferatu Mastah","rank":281,"voc":"Elite Knight","level":110},{"name":"Elitest Killah","rank":282,"voc":"Elite Knight","level":110},{"name":"Nemeruus","rank":283,"voc":"Elite Knight","level":110},{"name":"Dillaz antica","rank":284,"voc":"Elite Knight","level":110},{"name":"Sausagehead","rank":285,"voc":"Elite Knight","level":110},{"name":"Bhloxx","rank":286,"voc":"Knight","level":110},{"name":"Knight of Victory","rank":287,"voc":"Elite Knight","level":110},{"name":"Medo The Warlord","rank":288,"voc":"Knight","level":110},{"name":"Mihla","rank":289,"voc":"Elite Knight","level":110},{"name":"Zybex","rank":290,"voc":"Knight","level":110},{"name":"Andrzejka Marcinka","rank":291,"voc":"Knight","level":110},{"name":"Yedoy","rank":292,"voc":"Knight","level":110},{"name":"Katernooga","rank":293,"voc":"Elite Knight","level":110},{"name":"Borre The Knight","rank":294,"voc":"Elite Knight","level":110},{"name":"Sisqui","rank":295,"voc":"Elite Knight","level":110},{"name":"Kondamian","rank":296,"voc":"Elite Knight","level":110},{"name":"Wiadereczko Ocb","rank":297,"voc":"Elite Knight","level":110},{"name":"Pig of Doom","rank":298,"voc":"Knight","level":110},{"name":"Sillmasken","rank":299,"voc":"Elite Knight","level":110},{"name":"Zymorui","rank":300,"voc":"Knight","level":110}]},"information":{"api_version":2,"execution_time":0.12,"last_updated":"2019-01-07 23:52:59","timestamp":"2019-01-07 23:52:58"}} \ No newline at end of file diff --git a/tests/resources/highscores/tibiadata_loyalty.json b/tests/resources/highscores/tibiadata_loyalty.json new file mode 100644 index 00000000..6771df9a --- /dev/null +++ b/tests/resources/highscores/tibiadata_loyalty.json @@ -0,0 +1 @@ +{"highscores":{"world":"Zunera","type":"loyalty","data":[{"name":"Deackoy","rank":1,"voc":"Master Sorcerer","title":"Guardian of Tibia","points":4379},{"name":"Cassiandra","rank":2,"voc":"Sorcerer","title":"Guardian of Tibia","points":4227},{"name":"Lamb of Satan","rank":3,"voc":"Master Sorcerer","title":"Guardian of Tibia","points":4128},{"name":"Little Poncho","rank":4,"voc":"Master Sorcerer","title":"Keeper of Tibia","points":3842},{"name":"Jeanzitiz ho Pater","rank":5,"voc":"Sorcerer","title":"Keeper of Tibia","points":3620},{"name":"Nimbly Bimbly","rank":6,"voc":"Master Sorcerer","title":"Keeper of Tibia","points":3466},{"name":"Flor Dliz","rank":7,"voc":"Master Sorcerer","title":"Keeper of Tibia","points":3418},{"name":"Sticky Fingerz","rank":8,"voc":"Sorcerer","title":"Keeper of Tibia","points":3211},{"name":"Sath Arco","rank":9,"voc":"Sorcerer","title":"Keeper of Tibia","points":3169},{"name":"Lady Barber","rank":10,"voc":"Master Sorcerer","title":"Keeper of Tibia","points":3144},{"name":"Deylon Weirius","rank":11,"voc":"Sorcerer","title":"Keeper of Tibia","points":3114},{"name":"Drark Alven","rank":12,"voc":"Sorcerer","title":"Keeper of Tibia","points":3100},{"name":"Runup","rank":13,"voc":"Master Sorcerer","title":"Keeper of Tibia","points":3072},{"name":"Vedabro","rank":14,"voc":"Sorcerer","title":"Keeper of Tibia","points":3038},{"name":"Edu Santi","rank":15,"voc":"Master Sorcerer","title":"Keeper of Tibia","points":3010},{"name":"Lordnasa","rank":16,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2995},{"name":"Onimusha warrior","rank":17,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2946},{"name":"Caleb Skull","rank":18,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2934},{"name":"Tholtan","rank":19,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2892},{"name":"Swiifit Kamikaze","rank":20,"voc":"Sorcerer","title":"Warrior of Tibia","points":2865},{"name":"Hamburlgar","rank":21,"voc":"Sorcerer","title":"Warrior of Tibia","points":2799},{"name":"Straight Posted","rank":22,"voc":"Sorcerer","title":"Warrior of Tibia","points":2791},{"name":"Comandante Zdos","rank":23,"voc":"Sorcerer","title":"Warrior of Tibia","points":2754},{"name":"Phal Baeliah","rank":24,"voc":"Sorcerer","title":"Warrior of Tibia","points":2735},{"name":"Tuth Babarack","rank":25,"voc":"Sorcerer","title":"Warrior of Tibia","points":2710},{"name":"Astian Chaos","rank":26,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2611},{"name":"Pinder jeet","rank":27,"voc":"Sorcerer","title":"Warrior of Tibia","points":2593},{"name":"Sizzike","rank":28,"voc":"Sorcerer","title":"Warrior of Tibia","points":2561},{"name":"Raawsz","rank":29,"voc":"Sorcerer","title":"Warrior of Tibia","points":2550},{"name":"Kull","rank":30,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2534},{"name":"Tenebrosso","rank":31,"voc":"Sorcerer","title":"Warrior of Tibia","points":2507},{"name":"Ghaleb","rank":32,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2478},{"name":"Saint Adrian","rank":33,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2468},{"name":"Pikeno Thug","rank":34,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2451},{"name":"Dralkin Juanaki","rank":35,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2407},{"name":"Captain Johnny","rank":36,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2360},{"name":"Meliissa","rank":37,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2340},{"name":"Twoopaac","rank":38,"voc":"Sorcerer","title":"Warrior of Tibia","points":2340},{"name":"Lekko Rushback","rank":39,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2322},{"name":"Mood Oribin","rank":40,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2317},{"name":"Arwyn Carigos","rank":41,"voc":"Sorcerer","title":"Warrior of Tibia","points":2310},{"name":"Killer Kael","rank":42,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2289},{"name":"Famous Dedd","rank":43,"voc":"Sorcerer","title":"Warrior of Tibia","points":2287},{"name":"Wet willies","rank":44,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2280},{"name":"Nidope","rank":45,"voc":"Sorcerer","title":"Warrior of Tibia","points":2243},{"name":"Flip Nitro","rank":46,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2229},{"name":"Yurixx","rank":47,"voc":"Sorcerer","title":"Warrior of Tibia","points":2204},{"name":"Ed Hardly","rank":48,"voc":"Sorcerer","title":"Warrior of Tibia","points":2202},{"name":"Texxa","rank":49,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2191},{"name":"Shine spellcaster","rank":50,"voc":"Sorcerer","title":"Warrior of Tibia","points":2174},{"name":"Vdoviec","rank":51,"voc":"Sorcerer","title":"Warrior of Tibia","points":2168},{"name":"Zethes","rank":52,"voc":"Sorcerer","title":"Warrior of Tibia","points":2130},{"name":"Flaxmaids","rank":53,"voc":"Sorcerer","title":"Warrior of Tibia","points":2129},{"name":"Ashiw Reidelas","rank":54,"voc":"Sorcerer","title":"Warrior of Tibia","points":2123},{"name":"Sweeth Queen","rank":55,"voc":"Sorcerer","title":"Warrior of Tibia","points":2120},{"name":"Durysh Headshot","rank":56,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2110},{"name":"Chatoh The Ripper","rank":57,"voc":"Master Sorcerer","title":"Warrior of Tibia","points":2085}]},"information":{"api_version":2,"execution_time":0.0023,"last_updated":"2019-01-07 23:53:53","timestamp":"2019-01-07 23:54:18"}} \ No newline at end of file diff --git a/tests/tests_highscores.py b/tests/tests_highscores.py index c75844f6..50a72f22 100644 --- a/tests/tests_highscores.py +++ b/tests/tests_highscores.py @@ -1,12 +1,18 @@ +import tests.tests_character from tests.tests_tibiapy import TestTibiaPy -from tibiapy import Category, ExpHighscoresEntry, Highscores, HighscoresEntry, LoyaltyHighscoresEntry, Vocation, \ - VocationFilter +from tibiapy import Category, ExpHighscoresEntry, Highscores, HighscoresEntry, InvalidContent, LoyaltyHighscoresEntry, \ + Vocation, VocationFilter FILE_HIGHSCORES_FULL = "highscores/tibiacom_full.txt" FILE_HIGHSCORES_EXPERIENCE = "highscores/tibiacom_experience.txt" FILE_HIGHSCORES_LOYALTY = "highscores/tibiacom_loyalty.txt" FILE_HIGHSCORES_EMPTY = "highscores/tibiacom_empty.txt" +FILE_HIGHSCORES_TIBIADATA_FULL = "highscores/tibiadata_full.json" +FILE_HIGHSCORES_TIBIADATA_EXPERIENCE = "highscores/tibiadata_experience.json" +FILE_HIGHSCORES_TIBIADATA_LOYALTY = "highscores/tibiadata_loyalty.json" +FILE_HIGHSCORES_TIBIADATA_EMPTY = "highscores/tibiadata_empty.json" + class TestHighscores(TestTibiaPy): # region Tibia.com Tests @@ -18,6 +24,11 @@ def testHighscores(self): self.assertEqual(highscores.vocation, VocationFilter.KNIGHTS) self.assertEqual(highscores.category, Category.MAGIC_LEVEL) self.assertEqual(highscores.results_count, 300) + self.assertEqual(highscores.from_rank, 76) + self.assertEqual(highscores.to_rank, 100) + self.assertEqual(highscores.page, 4) + self.assertEqual(highscores.total_pages, 12) + self.assertIsNotNone(highscores.url) for entry in highscores.entries: self.assertIsInstance(entry, HighscoresEntry) @@ -34,6 +45,7 @@ def testHighscoresExperience(self): self.assertEqual(highscores.vocation, VocationFilter.PALADINS) self.assertEqual(highscores.category, Category.EXPERIENCE) self.assertEqual(highscores.results_count, 300) + self.assertEqual(highscores.total_pages, 12) for entry in highscores.entries: self.assertIsInstance(entry, ExpHighscoresEntry) @@ -51,6 +63,7 @@ def testHighscoresLoyalty(self): self.assertEqual(highscores.vocation, VocationFilter.PALADINS) self.assertEqual(highscores.category, Category.LOYALTY_POINTS) self.assertEqual(highscores.results_count, 65) + self.assertEqual(highscores.total_pages, 3) for entry in highscores.entries: self.assertIsInstance(entry, LoyaltyHighscoresEntry) @@ -66,4 +79,76 @@ def testHighscoresEmpty(self): self.assertIsNone(highscores) + def testHighscoresTibiaDataListUnrelated(self): + with self.assertRaises(InvalidContent): + Highscores.from_tibiadata(self._load_resource(tests.tests_character.FILE_CHARACTER_TIBIADATA)) + + def testHighscoresUnrelated(self): + content = self._load_resource(self.FILE_UNRELATED_SECTION) + with self.assertRaises(InvalidContent): + Highscores.from_content(content) + + # endregion + + # region TibiaData.com Tests + def testHighscoresTibiaData(self): + content = self._load_resource(FILE_HIGHSCORES_TIBIADATA_FULL) + highscores = Highscores.from_tibiadata(content) + + self.assertEqual(highscores.world, "Antica") + self.assertEqual(highscores.vocation, VocationFilter.ALL) + self.assertEqual(highscores.category, Category.AXE_FIGHTING) + self.assertEqual(highscores.results_count, 300) + + for entry in highscores.entries: + self.assertIsInstance(entry, HighscoresEntry) + self.assertIsInstance(entry.name, str) + self.assertIsInstance(entry.vocation, Vocation) + self.assertIsInstance(entry.rank, int) + self.assertIsInstance(entry.value, int) + + def testHighscoresTibiaDataExperience(self): + content = self._load_resource(FILE_HIGHSCORES_TIBIADATA_EXPERIENCE) + highscores = Highscores.from_tibiadata(content) + + self.assertEqual(highscores.world, "Luminera") + self.assertEqual(highscores.vocation, VocationFilter.ALL) + self.assertEqual(highscores.category, Category.EXPERIENCE) + self.assertEqual(highscores.results_count, 300) + + for entry in highscores.entries: + self.assertIsInstance(entry, ExpHighscoresEntry) + self.assertIsInstance(entry.name, str) + self.assertIsInstance(entry.vocation, Vocation) + self.assertIsInstance(entry.rank, int) + self.assertIsInstance(entry.value, int) + self.assertIsInstance(entry.level, int) + + def testHighscoresTibiaDataLoyalty(self): + content = self._load_resource(FILE_HIGHSCORES_TIBIADATA_LOYALTY) + highscores = Highscores.from_tibiadata(content) + + self.assertEqual(highscores.world, "Zunera") + self.assertEqual(highscores.vocation, VocationFilter.ALL) + self.assertEqual(highscores.category, Category.LOYALTY_POINTS) + self.assertEqual(highscores.results_count, 57) + + for entry in highscores.entries: + self.assertIsInstance(entry, LoyaltyHighscoresEntry) + self.assertIsInstance(entry.name, str) + self.assertIsInstance(entry.vocation, Vocation) + self.assertIsInstance(entry.rank, int) + self.assertIsInstance(entry.value, int) + self.assertIsInstance(entry.title, str) + + def testHighscoresTibaDataEmpty(self): + content = self._load_resource(FILE_HIGHSCORES_TIBIADATA_EMPTY) + highscores = Highscores.from_tibiadata(content) + + self.assertIsNone(highscores) + + def testHighscoresTibiaDataListUnrelated(self): + with self.assertRaises(InvalidContent): + Highscores.from_tibiadata(self._load_resource(tests.tests_character.FILE_CHARACTER_TIBIADATA)) + # endregion diff --git a/tests/tests_house.py b/tests/tests_house.py index c3e9d980..9c1c5fd9 100644 --- a/tests/tests_house.py +++ b/tests/tests_house.py @@ -26,12 +26,8 @@ class TestsHouse(TestTibiaPy): def setUp(self): self.guild = {} - @staticmethod - def _get_resource(resource): - return TestTibiaPy._load_resource(resource) - def testHouse(self): - content = self._get_resource(FILE_HOUSE_FULL) + content = self._load_resource(FILE_HOUSE_FULL) house = House.from_content(content) self.assertIsInstance(house, House) @@ -53,7 +49,7 @@ def testHouse(self): def testHouseStatusTransferred(self): house = House("Name") - content = self._get_resource(FILE_HOUSE_STATUS_TRANSFER) + content = self._load_resource(FILE_HOUSE_STATUS_TRANSFER) house._parse_status(content) self.assertEqual(house.status, HouseStatus.RENTED) self.assertEqual(house.owner, "Xenaris mag") @@ -64,7 +60,7 @@ def testHouseStatusTransferred(self): def testHouseStatusRented(self): house = House("Name") - content = self._get_resource(FILE_HOUSE_STATUS_RENTED) + content = self._load_resource(FILE_HOUSE_STATUS_RENTED) house._parse_status(content) self.assertEqual(house.status, HouseStatus.RENTED) self.assertEqual(house.owner, "Thorcen") @@ -72,7 +68,7 @@ def testHouseStatusRented(self): def testHouseStatusWithBids(self): house = House("Name") - content = self._get_resource(FILE_HOUSE_STATUS_WITH_BIDS) + content = self._load_resource(FILE_HOUSE_STATUS_WITH_BIDS) house._parse_status(content) self.assertEqual(house.status, HouseStatus.AUCTIONED) self.assertIsNone(house.owner) @@ -82,24 +78,24 @@ def testHouseStatusWithBids(self): def testHouseStatusWithoutBids(self): house = House("Name") - content = self._get_resource(FILE_HOUSE_STATUS_NO_BIDS) + content = self._load_resource(FILE_HOUSE_STATUS_NO_BIDS) house._parse_status(content) self.assertEqual(house.status, HouseStatus.AUCTIONED) self.assertIsNone(house.auction_end) def testHouseNotFound(self): - content = self._get_resource(FILE_HOUSE_NOT_FOUND) + content = self._load_resource(FILE_HOUSE_NOT_FOUND) house = House.from_content(content) self.assertIsNone(house) def testHouseUnrelated(self): - content = self._get_resource(self.FILE_UNRELATED_SECTION) + content = self._load_resource(self.FILE_UNRELATED_SECTION) with self.assertRaises(InvalidContent): House.from_content(content) def testHouseList(self): - content = self._get_resource(FILE_HOUSE_LIST) + content = self._load_resource(FILE_HOUSE_LIST) houses = ListedHouse.list_from_content(content) self.assertIsInstance(houses, list) @@ -115,24 +111,24 @@ def testHouseList(self): self.assertEqual(houses[25].highest_bid, 7500000) def testHouseListEmpty(self): - content = self._get_resource(FILE_HOUSE_LIST_EMPTY) + content = self._load_resource(FILE_HOUSE_LIST_EMPTY) houses = ListedHouse.list_from_content(content) self.assertEqual(len(houses), 0) def testHouseListNotFound(self): - content = self._get_resource(FILE_HOUSE_NOT_FOUND) + content = self._load_resource(FILE_HOUSE_NOT_FOUND) houses = ListedHouse.list_from_content(content) self.assertIsNone(houses) def testHouseListUnrelated(self): - content = self._get_resource(self.FILE_UNRELATED_SECTION) + content = self._load_resource(self.FILE_UNRELATED_SECTION) with self.assertRaises(InvalidContent): ListedHouse.list_from_content(content) def testHouseTibiaData(self): - content = self._get_resource(FILE_HOUSE_TIBIADATA) + content = self._load_resource(FILE_HOUSE_TIBIADATA) house = House.from_tibiadata(content) self.assertIsInstance(house, House) @@ -144,7 +140,7 @@ def testHouseTibiaData(self): self.assertIsNone(house.owner) def testHouseTibiaDataNotFound(self): - content = self._get_resource(FILE_HOUSE_TIBIADATA_NOT_FOUND) + content = self._load_resource(FILE_HOUSE_TIBIADATA_NOT_FOUND) house = House.from_tibiadata(content) self.assertIsNone(house) @@ -154,12 +150,12 @@ def testHouseTibiaDataInvalid(self): House.from_tibiadata("

Not json

") def testGuildTibiaDataUnrelated(self): - content = self._get_resource(tests.tests_character.FILE_CHARACTER_TIBIADATA) + content = self._load_resource(tests.tests_character.FILE_CHARACTER_TIBIADATA) with self.assertRaises(InvalidContent): House.from_tibiadata(content) def testHouseTibiaDataList(self): - content = self._get_resource(FILE_HOUSE_TIBIADATA_LIST) + content = self._load_resource(FILE_HOUSE_TIBIADATA_LIST) houses = ListedHouse.list_from_tibiadata(content) self.assertIsInstance(houses, list) @@ -172,7 +168,7 @@ def testHouseTibiaDataList(self): self.assertIsNotNone(ListedHouse.get_list_url_tibiadata(houses[0].world, houses[0].town)) def testHouseTibiaDataListNotFound(self): - content = self._get_resource(FILE_HOUSE_TIBIADATA_LIST_NOT_FOUND) + content = self._load_resource(FILE_HOUSE_TIBIADATA_LIST_NOT_FOUND) houses = ListedHouse.list_from_tibiadata(content) self.assertIsInstance(houses, list) @@ -184,4 +180,4 @@ def testHouseTibiaDataListInvalidJson(self): def testHouseTibiaDataListUnrelated(self): with self.assertRaises(InvalidContent): - ListedHouse.list_from_tibiadata(self._get_resource(tests.tests_character.FILE_CHARACTER_TIBIADATA)) + ListedHouse.list_from_tibiadata(self._load_resource(tests.tests_character.FILE_CHARACTER_TIBIADATA)) diff --git a/tibiapy/character.py b/tibiapy/character.py index c069b761..fe87d825 100644 --- a/tibiapy/character.py +++ b/tibiapy/character.py @@ -1,5 +1,4 @@ import datetime -import json import re import urllib.parse from collections import OrderedDict @@ -12,8 +11,8 @@ from tibiapy.errors import InvalidContent from tibiapy.guild import Guild from tibiapy.house import CharacterHouse -from tibiapy.utils import parse_tibia_date, parse_tibia_datetime, parse_tibiacom_content, parse_tibiadata_date, \ - parse_tibiadata_datetime, try_datetime, try_enum +from tibiapy.utils import parse_json, parse_tibia_date, parse_tibia_datetime, parse_tibiacom_content, \ + parse_tibiadata_date, parse_tibiadata_datetime, try_datetime, try_enum deleted_regexp = re.compile(r'([^,]+), will be deleted at (.*)') # Extracts the death's level and killers. @@ -238,10 +237,7 @@ def from_tibiadata(cls, content): ------ InvalidContent If content is not a JSON string of the Character response.""" - try: - json_content = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a valid json string.") + json_content = parse_json(content) char = cls() try: character = json_content["characters"] diff --git a/tibiapy/guild.py b/tibiapy/guild.py index dbeabf1b..c987b404 100644 --- a/tibiapy/guild.py +++ b/tibiapy/guild.py @@ -1,5 +1,4 @@ import datetime -import json import re import urllib.parse from collections import OrderedDict @@ -11,8 +10,8 @@ from tibiapy.enums import Vocation from tibiapy.errors import InvalidContent from tibiapy.house import GuildHouse -from tibiapy.utils import parse_tibia_date, parse_tibiacom_content, parse_tibiadata_date, try_date, try_datetime, \ - try_enum +from tibiapy.utils import parse_json, parse_tibia_date, parse_tibiacom_content, parse_tibiadata_date, try_date, \ + try_datetime, try_enum __all__ = ("Guild", "GuildMember", "GuildInvite", "ListedGuild") @@ -173,11 +172,7 @@ def from_tibiadata(cls, content): InvalidContent If content is not a JSON response of a guild's page. """ - try: - json_content = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a json string.") - + json_content = parse_json(content) guild = cls() try: guild_obj = json_content["guild"] @@ -543,10 +538,7 @@ def list_from_tibiadata(cls, content): InvalidContent If content is not a JSON response of TibiaData's guild list. """ - try: - json_content = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a valid json string.") + json_content = parse_json(content) try: guilds_obj = json_content["guilds"] guilds = [] diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py index 050fe007..409b93dc 100644 --- a/tibiapy/highscores.py +++ b/tibiapy/highscores.py @@ -1,15 +1,18 @@ +import math import re from collections import OrderedDict from typing import List from tibiapy import Category, InvalidContent, Vocation, VocationFilter, abc -from tibiapy.utils import parse_tibiacom_content, try_enum +from tibiapy.utils import parse_json, parse_tibiacom_content, try_enum __all__ = ("ExpHighscoresEntry", "Highscores", "HighscoresEntry", "LoyaltyHighscoresEntry") results_pattern = re.compile(r'Results: (\d+)') HIGHSCORES_URL = "https://secure.tibia.com/community/?subtopic=highscores&world=%s&list=%s&profession=%d¤tpage=%d" +HIGHSCORES_URL_TIBIADATA = "https://api.tibiadata.com/v2/highscores/%s/%s/%s.json" + class Highscores(abc.Serializable): """Represents the highscores of a world. @@ -19,7 +22,7 @@ class Highscores(abc.Serializable): Attributes ---------- - world: :class:`world` + world: :class:`str` The world the highscores belong to. category: :class:`Category` The selected category to displays the highscores of. @@ -51,18 +54,23 @@ def to_rank(self): @property def page(self): """:class:`int`: The page number the shown results correspond to on Tibia.com""" - return int(self.from_rank/25)+1 if self.from_rank else 0 + return int(math.floor(self.from_rank/25))+1 if self.from_rank else 0 @property def total_pages(self): """:class:`int`: The total of pages of the highscores category.""" - return int(self.results_count/25) + return int(math.ceil(self.results_count/25)) @property def url(self): """:class:`str`: The URL to the highscores page on Tibia.com containing the results.""" return self.get_url(self.world, self.category, self.vocation, self.page) + @property + def url_tibiadata(self): + """:class:`str`: The URL to the highscores page on TibiaData.com containing the results.""" + return self.get_url_tibiadata(self.world, self.category, self.vocation) + @classmethod def from_content(cls, content): """Creates an instance of the class from the html content of a highscores page. @@ -90,7 +98,7 @@ def from_content(cls, content): tables = cls._parse_tables(parsed_content) filters = tables.get("Highscores Filter") if filters is None: - raise InvalidContent() + raise InvalidContent("content does is not from the highscores section of Tibia.com") world_filter, vocation_filter, category_filter = filters world = world_filter.find("option", {"selected": True})["value"] if world == "": @@ -110,6 +118,61 @@ def from_content(cls, content): highscores._parse_entry(cols_raw) return highscores + @classmethod + def from_tibiadata(cls, content, vocation=None): + """Builds a highscores object from a TibiaData highscores response. + + Notes + ----- + Since TibiaData.com's response doesn't contain any indication of the vocation filter applied, + :py:attr:`vocation` can't be determined from the response, so the attribute must be assigned manually. + + If the attribute is known, it can be passed for it to be assigned in this method. + + Parameters + ---------- + content: :class:`str` + The JSON content of the response. + vocation: :class:`VocationFilter`, optional + The vocation filter to assign to the results. Note that this won't affect the parsing. + + Returns + ------- + :class:`Highscores` + The highscores contained in the page, or None if the content is for the highscores of a nonexistent world. + + Raises + ------ + InvalidContent + If content is not a JSON string of the highscores response.""" + json_content = parse_json(content) + try: + highscores_json = json_content["highscores"] + if "error" in highscores_json["data"]: + return None + world = highscores_json["world"] + category = highscores_json["type"] + highscores = cls(world, category) + for entry in highscores_json["data"]: + value_key = "level" + if highscores.category in [Category.ACHIEVEMENTS, Category.LOYALTY_POINTS, Category.EXPERIENCE]: + value_key = "points" + if highscores.category == Category.EXPERIENCE: + highscores.entries.append(ExpHighscoresEntry(entry["name"], entry["rank"], entry["voc"], + entry[value_key], entry["level"])) + elif highscores.category == Category.LOYALTY_POINTS: + highscores.entries.append(LoyaltyHighscoresEntry(entry["name"], entry["rank"], entry["voc"], + entry[value_key], entry["title"])) + else: + highscores.entries.append(HighscoresEntry(entry["name"], entry["rank"], entry["voc"], + entry[value_key])) + highscores.results_count = len(highscores.entries) + except KeyError: + raise InvalidContent("content is not a TibiaData highscores response.") + if isinstance(vocation, VocationFilter): + highscores.vocation = vocation + return highscores + @classmethod def get_url(cls, world, category=Category.EXPERIENCE, vocation=VocationFilter.ALL, page=1): """Gets the Tibia.com URL of the highscores for the given parameters. @@ -131,6 +194,25 @@ def get_url(cls, world, category=Category.EXPERIENCE, vocation=VocationFilter.AL """ return HIGHSCORES_URL % (world, category.value, vocation.value, page) + @classmethod + def get_url_tibiadata(cls, world, category=Category.EXPERIENCE, vocation=VocationFilter.ALL): + """Gets the TibiaData.com URL of the highscores for the given parameters. + + Parameters + ---------- + world: :class:`str` + The game world of the desired highscores. + category: :class:`Category` + The desired highscores category. + vocation: :class:`VocationFiler` + The vocation filter to apply. By default all vocations will be shown. + + Returns + ------- + The URL to the TibiaData.com highscores. + """ + return HIGHSCORES_URL_TIBIADATA % (world, category.value.lower(), vocation.name.lower()) + @classmethod def _parse_tables(cls, parsed_content): """ diff --git a/tibiapy/house.py b/tibiapy/house.py index 7dc7f60a..b310a016 100644 --- a/tibiapy/house.py +++ b/tibiapy/house.py @@ -1,5 +1,4 @@ import datetime -import json import re import urllib.parse from typing import Optional @@ -8,8 +7,8 @@ from tibiapy import abc from tibiapy.enums import HouseStatus, HouseType, Sex from tibiapy.errors import InvalidContent -from tibiapy.utils import parse_number_words, parse_tibia_datetime, parse_tibiacom_content, try_date, try_datetime, \ - try_enum +from tibiapy.utils import parse_json, parse_number_words, parse_tibia_datetime, parse_tibiacom_content, try_date, \ + try_datetime, try_enum __all__ = ("House", "CharacterHouse", "GuildHouse", "ListedHouse") @@ -191,10 +190,7 @@ def from_tibiadata(cls, content): InvalidContent If the content is not a house JSON response from TibiaData """ - try: - json_content = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a json string.") + json_content = parse_json(content) try: house_json = json_content["house"] if not house_json["name"]: @@ -424,10 +420,7 @@ def list_from_tibiadata(cls, content): InvalidContent` Content is not the house list from TibiaData.com """ - try: - json_data = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a json string") + json_data = parse_json(content) try: house_data = json_data["houses"] houses = [] diff --git a/tibiapy/utils.py b/tibiapy/utils.py index 8581173d..14e3df57 100644 --- a/tibiapy/utils.py +++ b/tibiapy/utils.py @@ -1,8 +1,11 @@ import datetime +import json from typing import Optional, Type, TypeVar, Union import bs4 +from tibiapy.errors import InvalidContent + def parse_tibia_datetime(datetime_str) -> Optional[datetime.datetime]: """Parses date and time from the format used in Tibia.com @@ -286,3 +289,39 @@ def try_enum(cls: Type[T], val, default: D = None) -> Union[T, D]: return cls(val) except ValueError: return default + + +def parse_json(content): + """Tries to parse a string into a json object. + + This also performs a trim of all values, recursively removing leading and trailing whitespace. + + Parameters + ---------- + content: A JSON format string. + + Returns + ------- + obj: + The object represented by the json string. + + Raises + ------ + InvalidContent + If the content is not a valid json string. + """ + try: + json_content = json.loads(content) + return _recursive_strip(json_content) + except json.JSONDecodeError: + raise InvalidContent("content is not a json string.") + + +def _recursive_strip(value): + if isinstance(value, dict): + return {k: _recursive_strip(v) for k, v in value.items()} + if isinstance(value, list): + return [_recursive_strip(i) for i in value] + if isinstance(value, str): + return value.strip() + return value diff --git a/tibiapy/world.py b/tibiapy/world.py index 62b0a313..b620a7f0 100644 --- a/tibiapy/world.py +++ b/tibiapy/world.py @@ -1,4 +1,3 @@ -import json import re from collections import OrderedDict from typing import List @@ -8,8 +7,8 @@ from tibiapy import InvalidContent, abc from tibiapy.character import OnlineCharacter from tibiapy.enums import PvpType, TransferType, WorldLocation -from tibiapy.utils import parse_tibia_datetime, parse_tibia_full_date, parse_tibiacom_content, parse_tibiadata_datetime, \ - try_date, try_datetime, try_enum +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 __all__ = ("ListedWorld", "World", "WorldOverview") @@ -276,10 +275,7 @@ def from_tibiadata(cls, content): InvalidContent If the provided content is not a TibiaData world response. """ - try: - json_data = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a valid json string.") + json_data = parse_json(content) try: world_data = json_data["world"] world_info = world_data["world_information"] @@ -514,10 +510,7 @@ def from_tibiadata(cls, content): InvalidContent If the provided content is the json content of the world section in TibiaData.com """ - try: - json_data = json.loads(content) - except json.JSONDecodeError: - raise InvalidContent("content is not a valid json string.") + json_data = parse_json(content) try: worlds_json = json_data["worlds"]["allworlds"] world_overview = cls() From 7a05f8428cfede68b738edde186c0a5616d915af Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Tue, 8 Jan 2019 09:59:14 -0700 Subject: [PATCH 07/12] Added changelog to documentation - Added type hints to many instance variables --- CHANGELOG.md | 15 --------- CHANGELOG.rst | 40 ++++++++++++++++++++++++ MANIFEST.in | 2 +- docs/changelog.rst | 1 + docs/conf.py | 3 +- docs/index.rst | 1 + setup.py | 20 +++++++++++- tests/resources/README.md | 11 +++++++ tibiapy/__init__.py | 2 +- tibiapy/character.py | 38 +++++++++++------------ tibiapy/guild.py | 38 +++++++++++------------ tibiapy/highscores.py | 10 +++--- tibiapy/house.py | 64 ++++++++++++++++++++------------------- tibiapy/world.py | 32 ++++++++++---------- 14 files changed, 168 insertions(+), 109 deletions(-) delete mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.rst create mode 100644 docs/changelog.rst diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 204e514f..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# Changelog -## Version 1.0.0 (2018-12-23) -- Added support for TibiaData JSON parsing. To have interoperability between Tibia.com and TibiaData. -- Added support for parsing Houses, House lists, World and World list -- Added support for many missing attributes in Character and Guilds. -- All objects are now serializable to JSON strings. - -## Version 0.1.0 (2018-08-17) -Initial release: -- Parses content from tibia.com - - Character pages - - Guild pages - - Guild list pages -- Parses content into JSON format strings. -- Parses content into Python objects. \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..a7deb1dd --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,40 @@ +========= +Changelog +========= + +.. _v1.1.0: + +1.1.0 (2019-01-08) +================== + +- Parsing Highscores from Tibia.com and TibiaData. +- Some strings from TibiaData had unpredictable trailing whitespaces, + all leading and trailing whitespaces are removed. +- Added type hints to many variables and methods. + +.. _v1.0.0: + +1.0.0 (2018-12-23) +================== + +- Added support for TibiaData JSON parsing. To have interoperability + between Tibia.com and TibiaData. +- Added support for parsing Houses, House lists, World and World list +- Added support for many missing attributes in Character and Guilds. +- All objects are now serializable to JSON strings. + +.. _v0.1.0: + +0.1.0 (2018-08-17) +================== + +Initial release: + +- Parses content from tibia.com + + - Character pages + - Guild pages + - Guild list pages + +- Parses content into JSON format strings. +- Parses content into Python objects. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 9302e9f2..322e223a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include requirements.txt include README.md -include CHANGELOG.md +include CHANGELOG.rst include LICENSE \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..4d7817ae --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index a2b698b3..1d94c5ba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -100,7 +100,8 @@ def setup(app): 'github_repo': 'tibia.py', 'github_type': 'star', 'fixed_sidebar': True, - 'travis_button': True + 'travis_button': True, + 'donate_url': 'https://beerpay.io/Galarzaa90/tibia.py' } # Add any paths that contain custom static files (such as style sheets) here, diff --git a/docs/index.rst b/docs/index.rst index 890b2698..1aa93810 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ Indices and tables intro api + changelog * :ref:`genindex` * :ref:`search` diff --git a/setup.py b/setup.py index f97053c1..9414fa2a 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,24 @@ def get_version(package): return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) +version = get_version("tibiapy") +if version.endswith(('a', 'b', 'rc')): + # append version identifier based on commit count + try: + import subprocess + p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version += out.decode('utf-8').strip() + p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version += '+g' + out.decode('utf-8').strip() + except Exception: + pass + with open('requirements.txt') as f: requirements = f.read().splitlines() @@ -33,7 +51,7 @@ def get_version(package): setup( name='tibia.py', - version=get_version("tibiapy"), + version=version, author='Galarzaa90', author_email="allan.galarza@gmail.com", url='https://github.com/Galarzaa90/tibia.py', diff --git a/tests/resources/README.md b/tests/resources/README.md index 3a017aa8..7652acfb 100644 --- a/tests/resources/README.md +++ b/tests/resources/README.md @@ -54,6 +54,17 @@ exist. - [tibiadata_list.json](house/tibiadata_list.json) - A house list on TibiaData. - [tibiadata_list_not_found.json](house/tibiadata_list_not_found.json) - The TibiaData response for a house list that doesn't exist. + +## Highscores resources +- [tibiacom_full.txt](highscores/tibiacom_full.txt) - The content of a correct highscore's pagem +- [tibiacom_empty.txt](highscores/tibiacom_empty.txt) - The content of the highscores page of a nonexistent world. +- [tibiacom_experience.txt](highscores/tibiacom_experience.txt) - The content of an experience highscores page. +- [tibiacom_loyalty.txt](highscores/tibiacom_loyalty.txt) - The content of a loyalty highscores page. +- [tibiadata_full.json](highscores/tibiadata_full.json) - A highscores response from TibiaData. +- [tibiadata_empty.json](highscores/tibiadata_empty.json) - A highscores response from TibiaData of a nonexistent world. +- [tibiadata_experience.json](highscores/tibiadata_experience.json) - A response containing experience highscores from +TibiaData. +- [tibiadata_loyalty.json](highscores/tibiadata_loyalty.json) - A response containing loyalty highscores from TibiaData. ## World resources - [tibiacom_online.txt](world/tibiacom_online.txt) - An online world on Tibia.com. diff --git a/tibiapy/__init__.py b/tibiapy/__init__.py index b6bd0770..a9bef570 100644 --- a/tibiapy/__init__.py +++ b/tibiapy/__init__.py @@ -7,4 +7,4 @@ from tibiapy.house import * from tibiapy.world import * -__version__ = '1.0.0' +__version__ = '1.1.0rc' diff --git a/tibiapy/character.py b/tibiapy/character.py index fe87d825..fa7204ae 100644 --- a/tibiapy/character.py +++ b/tibiapy/character.py @@ -48,9 +48,9 @@ class AccountInformation(abc.Serializable): __slots__ = ("created", "loyalty_title", "position") def __init__(self, created, loyalty_title=None, position=None): - self.created = created - self.loyalty_title = loyalty_title - self.position = position + self.created = try_datetime(created) + self.loyalty_title = loyalty_title # type: Optional[str] + self.position = position # type: Optional[str] def __repr__(self): return "<%s created=%r>" % (self.__class__.__name__, self.created) @@ -69,8 +69,8 @@ class Achievement(abc.Serializable): __slots__ = ("name", "grade") def __init__(self, name, grade): - self.name = name - self.grade = grade + self.name = name # type: str + self.grade = int(grade) def __repr__(self): return "<%s name=%r grade=%d>" % (self.__class__.__name__, self.name, self.grade) @@ -134,8 +134,8 @@ def __init__(self, name=None, world=None, vocation=None, level=0, sex=None, **kw self.former_names = kwargs.get("former_names", []) # type: List[str] self.sex = try_enum(Sex, sex) self.vocation = try_enum(Vocation, vocation) - self.level = level # type: int - self.achievement_points = kwargs.get("achievement_points", 0) # type: int + self.level = int(level) + self.achievement_points = int(kwargs.get("achievement_points", 0)) self.world = world # type: str self.former_world = kwargs.get("former_world") # type: Optional[str] self.residence = kwargs.get("residence") # type: str @@ -622,8 +622,8 @@ class GuildMembership(abc.BaseGuild): __slots__ = ("rank",) def __init__(self, name, rank): - self.name = name - self.rank = rank + self.name = name # type: str + self.rank = rank # type: str def __repr__(self): return "<{0.__class__.__name__} name={0.name!r} rank={0.rank!r}>".format(self) @@ -651,9 +651,9 @@ class Killer(abc.Serializable): __slots__ = ("name", "player", "summon") def __init__(self, name, player=False, summon=None): - self.name = name - self.player = player - self.summon = summon + self.name = name # type: str + self.player = player # type: bool + self.summon = summon # type: Optional[str] def __repr__(self): attributes = "" @@ -695,11 +695,11 @@ class OtherCharacter(abc.BaseCharacter): """ __slots__ = ("world", "online", "deleted") - def __init__(self, name, world=None, online=False, deleted=False): - self.name = name - self.world = world - self.online = online - self.deleted = deleted + def __init__(self, name, world, online=False, deleted=False): + self.name = name # type: str + self.world = world # type: str + self.online = online # type: bool + self.deleted = deleted # type: bool class OnlineCharacter(abc.BaseCharacter): @@ -719,7 +719,7 @@ class OnlineCharacter(abc.BaseCharacter): __slots__ = ("world", "vocation", "level") def __init__(self, name, world, level, vocation): - self.name = name - self.world = world + self.name = name # type: str + self.world = world # type: str self.level = int(level) self.vocation = try_enum(Vocation, vocation) diff --git a/tibiapy/guild.py b/tibiapy/guild.py index c987b404..bf64a021 100644 --- a/tibiapy/guild.py +++ b/tibiapy/guild.py @@ -68,17 +68,17 @@ class Guild(abc.BaseGuild): "disband_condition", "disband_date", "homepage", "members", "invites") def __init__(self, name=None, world=None, **kwargs): - self.name = name - self.world = world - self.logo_url = kwargs.get("logo_url") - self.description = kwargs.get("description") + self.name = name # type: str + self.world = world # type: str + self.logo_url = kwargs.get("logo_url") # type: str + self.description = kwargs.get("description") # type: Optional[str] self.founded = try_date(kwargs.get("founded")) - self.active = kwargs.get("active", False) + self.active = kwargs.get("active", False) # type: bool self.guildhall = kwargs.get("guildhall") # type: Optional[GuildHouse] - self.open_applications = kwargs.get("open_applications", False) - self.disband_condition = kwargs.get("disband_condition") + self.open_applications = kwargs.get("open_applications", False) # type: bool + self.disband_condition = kwargs.get("disband_condition") # type: Optional[str] self.disband_date = try_datetime(kwargs.get("disband_date")) - self.homepage = kwargs.get("homepage") + self.homepage = kwargs.get("homepage") # type: Optional[str] self.members = kwargs.get("members", []) # type: List[GuildMember] self.invites = kwargs.get("invites", []) # type: List[GuildInvite] @@ -381,12 +381,12 @@ class GuildMember(abc.BaseCharacter): __slots__ = ("name", "rank", "title", "level", "vocation", "joined", "online") def __init__(self, name=None, rank=None, title=None, level=0, vocation=None, **kwargs): - self.name = name - self.rank = rank - self.title = title + self.name = name # type: str + self.rank = rank # type: str + self.title = title # type: Optional[str] self.vocation = try_enum(Vocation, vocation) - self.level = level - self.online = kwargs.get("online", False) + self.level = int(level) + self.online = kwargs.get("online", False) # type: bool self.joined = try_date(kwargs.get("joined")) @@ -404,7 +404,7 @@ class GuildInvite(abc.BaseCharacter): __slots__ = ("date", ) def __init__(self, name=None, date=None): - self.name = name + self.name = name # type: str self.date = try_date(date) def __repr__(self): @@ -432,11 +432,11 @@ class ListedGuild(abc.BaseGuild): __slots__ = ("logo_url", "description", "world", "active") def __init__(self, name, world, logo_url=None, description=None, active=False): - self.name = name - self.world = world - self.logo_url = logo_url - self.description = description - self.active = active + self.name = name # type: str + self.world = world # type: str + self.logo_url = logo_url # type: str + self.description = description # type: Optional[str] + self.active = active # type: bool # region Public methods @classmethod diff --git a/tibiapy/highscores.py b/tibiapy/highscores.py index 409b93dc..7c007d9c 100644 --- a/tibiapy/highscores.py +++ b/tibiapy/highscores.py @@ -276,10 +276,10 @@ class HighscoresEntry(abc.BaseCharacter): value: :class:`int` The character's value for the highscores.""" def __init__(self, name, rank, vocation, value): - self.name = name - self.rank = rank + self.name = name # type: str + self.rank = rank # type: int self.vocation = try_enum(Vocation, vocation) - self.value = value + self.value = value # type: int def __repr__(self) -> str: return "<{0.__class__.__name__} rank={0.rank} name={0.name!r} value={0.value}>".format(self) @@ -302,7 +302,7 @@ class ExpHighscoresEntry(HighscoresEntry): The character's level.""" def __init__(self, name, rank, vocation, value, level): super().__init__(name, rank, vocation, value) - self.level = level + self.level = level # type: int class LoyaltyHighscoresEntry(HighscoresEntry): @@ -322,4 +322,4 @@ class LoyaltyHighscoresEntry(HighscoresEntry): The character's loyalty title.""" def __init__(self, name, rank, vocation, value, title): super().__init__(name, rank, vocation, value) - self.title = title + self.title = title # type: str diff --git a/tibiapy/house.py b/tibiapy/house.py index b310a016..c89d60c2 100644 --- a/tibiapy/house.py +++ b/tibiapy/house.py @@ -78,24 +78,24 @@ class House(abc.BaseHouseWithId): "highest_bidder", "auction_end") def __init__(self, name, world=None, **kwargs): - self.id = kwargs.get("id", 0) - self.name = name - self.world = world - self.image_url = kwargs.get("image_url") - self.beds = kwargs.get("beds", 0) + self.id = kwargs.get("id", 0) # type: int + self.name = name # type: str + self.world = world # type: str + self.image_url = kwargs.get("image_url") # type: str + self.beds = kwargs.get("beds", 0) # type: int self.type = try_enum(HouseType, kwargs.get("type"), HouseType.HOUSE) - self.size = kwargs.get("size", 0) - self.rent = kwargs.get("rent", 0) + self.size = kwargs.get("size", 0) # type: int + self.rent = kwargs.get("rent", 0) # type: int self.status = try_enum(HouseStatus, kwargs.get("status"), None) - self.owner = kwargs.get("owner") + self.owner = kwargs.get("owner") # type: Optional[str] self.owner_sex = try_enum(Sex, kwargs.get("owner_sex")) self.paid_until = try_datetime(kwargs.get("paid_until")) self.transfer_date = try_datetime(kwargs.get("transfer_date")) - self.transferee = kwargs.get("transferee") - self.transfer_price = kwargs.get("transfer_price", 0) - self.transfer_accepted = kwargs.get("transfer_accepted", False) - self.highest_bid = kwargs.get("highest_bid", 0) - self.highest_bidder = kwargs.get("highest_bidder") + self.transferee = kwargs.get("transferee") #type: Optional[str] + self.transfer_price = kwargs.get("transfer_price", 0) # type: int + self.transfer_accepted = kwargs.get("transfer_accepted", False) # type: bool + self.highest_bid = kwargs.get("highest_bid", 0) # type: int + self.highest_bidder = kwargs.get("highest_bidder") # type: Optional[str] self.auction_end = try_datetime(kwargs.get("auction_end")) # region Properties @@ -273,10 +273,10 @@ class CharacterHouse(abc.BaseHouseWithId): def __init__(self, _id, name, world=None, town=None, owner=None, paid_until_date=None): self.id = int(_id) - self.name = name - self.town = town - self.world = world - self.owner = owner + self.name = name # type: str + self.town = town # type: str + self.world = world # type: str + self.owner = owner # type: str self.paid_until_date = try_date(paid_until_date) self.status = HouseStatus.RENTED self.type = HouseType.HOUSE @@ -296,14 +296,16 @@ class GuildHouse(abc.BaseHouse): type: :class:`HouseType` The type of the house. owner: :class:`str` - The owner of the guildhall.""" + The owner of the guildhall. + paid_until_date: :class:`datetime.date` + The date the last paid rent is due.""" __slots__ = ("owner", "paid_until_date") def __init__(self, name, world=None, owner=None, paid_until_date=None): - self.name = name - self.world = world - self.owner = owner - self.paid_until_date = paid_until_date + self.name = name # type: str + self.world = world # type: str + self.owner = owner # type: str + self.paid_until_date = try_date(paid_until_date) self.status = HouseStatus.RENTED self.type = HouseType.GUILDHALL @@ -341,16 +343,16 @@ class ListedHouse(abc.BaseHouseWithId): __slots__ = ("town", "size", "rent", "time_left", "highest_bid") def __init__(self, name, world, houseid, **kwargs): - self.name = name - self.id = houseid - self.world = world - self.status = kwargs.get("status") - self.type = kwargs.get("type") - self.town = kwargs.get("town") - self.size = kwargs.get("size", 0) - self.rent = kwargs.get("rent", 0) + self.name = name # type: str + self.id = int(houseid) + self.world = world # type: str + self.status = try_enum(HouseStatus, kwargs.get("status")) + self.type = try_enum(HouseType, kwargs.get("type")) + self.town = kwargs.get("town") # type: str + self.size = kwargs.get("size", 0) # type int + self.rent = kwargs.get("rent", 0) # type: int self.time_left = kwargs.get("time_left") # type: Optional[datetime.timedelta] - self.highest_bid = kwargs.get("highest_bid", 0) + self.highest_bid = kwargs.get("highest_bid", 0) # type: int # region Public methods @classmethod diff --git a/tibiapy/world.py b/tibiapy/world.py index b620a7f0..44e3602c 100644 --- a/tibiapy/world.py +++ b/tibiapy/world.py @@ -44,16 +44,16 @@ class ListedWorld(abc.BaseWorld): Whether only premium account players are allowed to play in this server. """ def __init__(self, name, location=None, pvp_type=None, **kwargs): - self.name = name + self.name = name # type: str self.location = try_enum(WorldLocation, location) self.pvp_type = try_enum(PvpType, pvp_type) - self.status = kwargs.get("status") - self.online_count = kwargs.get("online_count", 0) + self.status = kwargs.get("status") # type: str + self.online_count = kwargs.get("online_count", 0) # type: int self.transfer_type = try_enum(TransferType, kwargs.get("transfer_type", TransferType.REGULAR)) - self.battleye_protected = kwargs.get("battleye_protected", False) + self.battleye_protected = kwargs.get("battleye_protected", False) # type: bool self.battleye_date = try_date(kwargs.get("battleye_date")) - self.experimental = kwargs.get("experimental") - self.premium_only = kwargs.get("premium_only", False) + self.experimental = kwargs.get("experimental", False) # type: bool + self.premium_only = kwargs.get("premium_only", False) # type: bool # region Public methods @classmethod @@ -187,21 +187,21 @@ class World(abc.BaseWorld): __slots__ = ("record_count", "record_date", "creation_date", "world_quest_titles", "online_players") def __init__(self, name, location=None, pvp_type=None, **kwargs): - self.name = name + self.name = name # type: str self.location = try_enum(WorldLocation, location) self.pvp_type = try_enum(PvpType, pvp_type) - self.status = kwargs.get("status") - self.online_count = kwargs.get("online_count", 0) - self.record_count = kwargs.get("record_count", 0) + self.status = kwargs.get("status") # type: bool + self.online_count = kwargs.get("online_count", 0) # type: int + self.record_count = kwargs.get("record_count", 0) # type: int self.record_date = try_datetime(kwargs.get("record_date")) - self.creation_date = kwargs.get("creation_date") + self.creation_date = kwargs.get("creation_date") # type: str self.transfer_type = try_enum(TransferType, kwargs.get("transfer_type", TransferType.REGULAR)) - self.world_quest_titles = kwargs.get("world_quest_titles", []) - self.battleye_protected = kwargs.get("battleye_protected", False) + self.world_quest_titles = kwargs.get("world_quest_titles", []) # type: List[str] + self.battleye_protected = kwargs.get("battleye_protected", False) # type: bool self.battleye_date = try_date(kwargs.get("battleye_date")) - self.experimental = kwargs.get("experimental") + self.experimental = kwargs.get("experimental", False) # type: bool self.online_players = kwargs.get("online_players", []) # type: List[OnlineCharacter] - self.premium_only = kwargs.get("premium_only", False) + self.premium_only = kwargs.get("premium_only", False) # type: bool # region Properties @property @@ -408,7 +408,7 @@ class WorldOverview(abc.Serializable): __slots__ = ("record_count", "record_date", "worlds") def __init__(self, **kwargs): - self.record_count = kwargs.get("record_count", 0) + self.record_count = kwargs.get("record_count", 0) # type: int self.record_date = try_datetime(kwargs.get("record_date")) self.worlds = kwargs.get("worlds", []) # type: List[ListedWorld] From d467e31360f57aeb16c681c9f9480fd3b57a23e1 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Tue, 8 Jan 2019 11:12:24 -0700 Subject: [PATCH 08/12] Added from_name method for VocationFilter --- docs/api.rst | 2 ++ tibiapy/enums.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index e3d34d56..553a053f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -48,6 +48,8 @@ Enumerations are provided for various values in order to avoid depending on stri :members: :undoc-members: + .. automethod:: VocationFilter.from_name + .. autoclass:: WorldLocation :members: :undoc-members: diff --git a/tibiapy/enums.py b/tibiapy/enums.py index d90acf05..0dc5869c 100644 --- a/tibiapy/enums.py +++ b/tibiapy/enums.py @@ -86,6 +86,30 @@ class VocationFilter(Enum): SORCERERS = 3 DRUIDS = 4 + @classmethod + def from_name(cls, name, all_fallback=True): + """Gets a vocation filter from a vocation's name. + + Parameters + ---------- + name: :class:`str` + The name of the vocation. + all_fallback: :class:`bool` + Whether to return :py:attr:`ALL` if no match is found. Otherwise, ``None`` will be returned. + + Returns + ------- + VocationFilter, optional: + The matching vocation filter. + """ + name = name.upper() + for vocation in cls: # type: VocationFilter + if vocation.name in name or vocation.name[:-1] in name and vocation != cls.ALL: + return vocation + if all_fallback or name.upper() == "ALL": + return cls.ALL + return None + class WorldLocation(BaseEnum): """The possible physical locations for servers.""" From c375fa3ea746f2d590bfe6c917ffb1d7a1e19112 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Tue, 8 Jan 2019 14:05:39 -0700 Subject: [PATCH 09/12] Added missing Category value shielding --- tibiapy/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tibiapy/enums.py b/tibiapy/enums.py index 0dc5869c..ead81b21 100644 --- a/tibiapy/enums.py +++ b/tibiapy/enums.py @@ -26,6 +26,7 @@ class Category(BaseEnum): FIST_FIGHTING = "fist" LOYALTY_POINTS = "loyalty" MAGIC_LEVEL = "magic" + SHIELDING = "shielding" SWORD_FIGHTING = "sword" From e8a53a2ba4e61b9be93573fb12abd5ec54c4cbc0 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 9 Jan 2019 09:17:02 -0700 Subject: [PATCH 10/12] Preparing version --- tibiapy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tibiapy/__init__.py b/tibiapy/__init__.py index a9bef570..15c5fdaf 100644 --- a/tibiapy/__init__.py +++ b/tibiapy/__init__.py @@ -7,4 +7,4 @@ from tibiapy.house import * from tibiapy.world import * -__version__ = '1.1.0rc' +__version__ = '1.1.0' From c8c5b71f1d6e430ee406a760d7d1a0d05d366d34 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 9 Jan 2019 09:18:33 -0700 Subject: [PATCH 11/12] Fixed release date --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a7deb1dd..6f22d114 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog .. _v1.1.0: -1.1.0 (2019-01-08) +1.1.0 (2019-01-09) ================== - Parsing Highscores from Tibia.com and TibiaData. From e5908a853bbe92d31faeb2b2c729b9e6442eea30 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 9 Jan 2019 09:28:24 -0700 Subject: [PATCH 12/12] Added unit tests for VocationFilter.from_name --- tests/tests_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/tests_utils.py b/tests/tests_utils.py index a9b1c59e..9025d39e 100644 --- a/tests/tests_utils.py +++ b/tests/tests_utils.py @@ -130,3 +130,6 @@ def testTryEnum(self): def testEnumStr(self): self.assertEqual(str(enums.Sex.MALE), enums.Sex.MALE.value) + 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))