diff --git a/.travis.yml b/.travis.yml
index 617bd4e0..e1dc9770 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:
@@ -29,6 +29,7 @@ after_script:
- coverage report
- coverage xml
- 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
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..6f22d114
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,40 @@
+=========
+Changelog
+=========
+
+.. _v1.1.0:
+
+1.1.0 (2019-01-09)
+==================
+
+- 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/api.rst b/docs/api.rst
index 97842a9b..553a053f 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,12 @@ Enumerations are provided for various values in order to avoid depending on stri
:members:
:undoc-members:
+.. autoclass:: VocationFilter
+ :members:
+ :undoc-members:
+
+ .. automethod:: VocationFilter.from_name
+
.. autoclass:: WorldLocation
:members:
:undoc-members:
@@ -61,6 +71,12 @@ Guild
:members:
:inherited-members:
+Highscores
+----------
+.. autoclass:: Highscores
+ :members:
+ :inherited-members:
+
House
-----
.. autoclass:: House
@@ -120,6 +136,12 @@ CharacterHouse
:members:
:inherited-members:
+ExpHighscoresEntry
+-------------------------
+.. autoclass:: ExpHighscoresEntry
+ :members:
+ :inherited-members:
+
Death
-----
.. autoclass:: Death
@@ -150,12 +172,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/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 5baa2ed3..1d94c5ba 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -96,9 +96,12 @@ 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,
+ '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/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 @@
+
+
Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).
+
\ No newline at end of file
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 @@
+
+
Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).
| Rank | Name | Vocation | Level | Points | | 1 | Jahcure | Royal Paladin | 787 | 8,083,013,963 | | 2 | Sir Fenix Galaxy | Royal Paladin | 760 | 7,263,651,916 | | 3 | Ragnar Inc | Royal Paladin | 759 | 7,254,728,435 | | 4 | Show dan | Royal Paladin | 727 | 6,372,307,887 | | 5 | Hell Puma | Royal Paladin | 699 | 5,652,173,922 | | 6 | Kirito Mito | Royal Paladin | 690 | 5,448,964,469 | | 7 | Nostro Ademus | Royal Paladin | 689 | 5,424,728,234 | | 8 | Naastrid | Royal Paladin | 661 | 4,777,584,954 | | 9 | Beboyy | Royal Paladin | 660 | 4,761,690,897 | | 10 | Aght Terina | Royal Paladin | 634 | 4,213,832,350 | | 11 | Freestylin | Royal Paladin | 621 | 3,957,553,125 | | 12 | Lapa Da Tank | Royal Paladin | 619 | 3,927,591,080 | | 13 | Moreno Fidera | Royal Paladin | 615 | 3,842,085,201 | | 14 | Akarians Demons | Royal Paladin | 610 | 3,750,276,607 | | 15 | Rich Ventura | Royal Paladin | 593 | 3,451,884,136 | | 16 | Mad Woman Fail | Royal Paladin | 589 | 3,373,205,570 | | 17 | Rahki | Paladin | 588 | 3,360,919,357 | | 18 | Mi Nameless | Royal Paladin | 584 | 3,290,870,583 | | 19 | Hell Lancer | Royal Paladin | 578 | 3,185,146,346 | | 20 | Haojrs | Royal Paladin | 576 | 3,164,770,018 | | 21 | Lord of Coxinha | Royal Paladin | 574 | 3,123,723,539 | | 22 | Salladix | Royal Paladin | 570 | 3,063,862,317 | | 23 | Ezudux | Royal Paladin | 553 | 2,792,304,037 | | 24 | Doggydig | Royal Paladin | 545 | 2,669,705,616 | | 25 | Toxikow | Paladin | 535 | 2,523,902,188 | » Results: 300 | | |
+
+
\ No newline at end of file
diff --git a/tests/resources/highscores/tibiacom_full.txt b/tests/resources/highscores/tibiacom_full.txt
new file mode 100644
index 00000000..ac2538e6
--- /dev/null
+++ b/tests/resources/highscores/tibiacom_full.txt
@@ -0,0 +1,4 @@
+
+
Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).
+
+
\ 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 @@
+
+
Skills displayed in the Highscores do not include any bonuses (loyalty, equipment etc.).
| Rank | Name | Vocation | Title | Points | | 1 | Scrumdiliumpious | Royal Paladin | Sage of Tibia | 5,608 | | 2 | Dawox | Royal Paladin | Sage of Tibia | 5,227 | | 3 | Alexzander The Great | Royal Paladin | Sage of Tibia | 5,110 | | 4 | Tazise | Royal Paladin | Sage of Tibia | 5,108 | | 5 | Mickeys | Royal Paladin | Sage of Tibia | 5,011 | | 6 | La Farge | Paladin | Guardian of Tibia | 4,929 | | 7 | Maria Tereza | Paladin | Guardian of Tibia | 4,839 | | 8 | Ninja Beta | Royal Paladin | Guardian of Tibia | 4,684 | | 9 | Azaziel | Royal Paladin | Guardian of Tibia | 4,554 | | 10 | Sara Nith | Paladin | Guardian of Tibia | 4,465 | | 11 | Tokfia | Royal Paladin | Guardian of Tibia | 4,449 | | 12 | Chrome Cougar | Royal Paladin | Guardian of Tibia | 4,324 | | 13 | Sejalzor | Royal Paladin | Guardian of Tibia | 4,202 | | 14 | Sir Dzidek | Royal Paladin | Guardian of Tibia | 4,183 | | 15 | Kaly Shie | Royal Paladin | Guardian of Tibia | 4,119 | | 16 | Amber Wolfhaven | Paladin | Guardian of Tibia | 4,097 | | 17 | Mauslayer | Royal Paladin | Guardian of Tibia | 4,056 | | 18 | Yathavek Sapervarus | Paladin | Keeper of Tibia | 3,973 | | 19 | Loyal Paladin Of Calmera | Royal Paladin | Keeper of Tibia | 3,948 | | 20 | Shanix Poxa | Paladin | Keeper of Tibia | 3,922 | | 21 | Neon Hero | Paladin | Keeper of Tibia | 3,886 | | 22 | Cantinelle | Royal Paladin | Keeper of Tibia | 3,885 | | 23 | Greens Arrow | Royal Paladin | Keeper of Tibia | 3,853 | | 24 | Rawman | Paladin | Keeper of Tibia | 3,765 | | 25 | Rocktron | Royal Paladin | Keeper of Tibia | 3,631 | » Results: 65 | | |
+
\ 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
new file mode 100644
index 00000000..50a72f22
--- /dev/null
+++ b/tests/tests_highscores.py
@@ -0,0 +1,154 @@
+import tests.tests_character
+from tests.tests_tibiapy import TestTibiaPy
+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
+ def testHighscores(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)
+ 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)
+ 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)
+ highscores = Highscores.from_content(content)
+
+ self.assertEqual(highscores.world, "Gladera")
+ 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)
+ 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)
+ highscores = Highscores.from_content(content)
+
+ self.assertEqual(highscores.world, "Calmera")
+ 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)
+ 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)
+
+ 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/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))
diff --git a/tibiapy/__init__.py b/tibiapy/__init__.py
index 68af4d33..15c5fdaf 100644
--- a/tibiapy/__init__.py
+++ b/tibiapy/__init__.py
@@ -3,7 +3,8 @@
from tibiapy.enums import *
from tibiapy.errors import *
from tibiapy.guild import *
+from tibiapy.highscores import *
from tibiapy.house import *
from tibiapy.world import *
-__version__ = '1.0.0'
+__version__ = '1.1.0'
diff --git a/tibiapy/character.py b/tibiapy/character.py
index 6c023d68..fa7204ae 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.
@@ -49,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)
@@ -70,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)
@@ -131,23 +130,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 = 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
+ 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]
@@ -239,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"]
@@ -627,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)
@@ -656,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 = ""
@@ -700,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):
@@ -724,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/enums.py b/tibiapy/enums.py
index eaacee2a..ead81b21 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,21 @@ class AccountStatus(BaseEnum):
PREMIUM_ACCOUNT = "Premium Account"
+class Category(BaseEnum):
+ """The different highscores categories."""
+ ACHIEVEMENTS = "achievements"
+ AXE_FIGHTING = "axe"
+ CLUB_FIGHTING = "club"
+ DISTANCE_FIGHTING = "distance"
+ EXPERIENCE = "experience"
+ FISHING = "fishing"
+ FIST_FIGHTING = "fist"
+ LOYALTY_POINTS = "loyalty"
+ MAGIC_LEVEL = "magic"
+ SHIELDING = "shielding"
+ SWORD_FIGHTING = "sword"
+
+
class HouseStatus(BaseEnum):
"""Renting statuses of a house."""
RENTED = "rented"
@@ -63,6 +79,39 @@ class Vocation(BaseEnum):
MASTER_SORCERER = "Master Sorcerer"
+class VocationFilter(Enum):
+ """The vocation filters available for Highscores."""
+ ALL = 0
+ KNIGHTS = 1
+ PALADINS = 2
+ 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."""
EUROPE = "Europe"
diff --git a/tibiapy/guild.py b/tibiapy/guild.py
index dbeabf1b..bf64a021 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")
@@ -69,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]
@@ -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"]
@@ -386,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"))
@@ -409,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):
@@ -437,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
@@ -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
new file mode 100644
index 00000000..7c007d9c
--- /dev/null
+++ b/tibiapy/highscores.py
@@ -0,0 +1,325 @@
+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_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.
+
+ Tibia.com only shows 25 entries per page.
+ TibiaData.com shows all results at once.
+
+ Attributes
+ ----------
+ world: :class:`str`
+ 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 # 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", []) # 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(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(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.
+
+ 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")
+ if filters is None:
+ 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 == "":
+ 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:
+ 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')
+ 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.
+
+ 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 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):
+ """
+ Parses the information tables found in a highscores 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
+
+ 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:
+ extra, value = values
+ else:
+ value, *extra = values
+ value = int(value.replace(',', ''))
+ 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 # type: str
+ self.rank = rank # type: int
+ self.vocation = try_enum(Vocation, vocation)
+ 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)
+
+
+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 # type: int
+
+
+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 # type: str
diff --git a/tibiapy/house.py b/tibiapy/house.py
index 7dc7f60a..c89d60c2 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")
@@ -79,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
@@ -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"]:
@@ -277,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
@@ -300,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
@@ -345,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
@@ -424,10 +422,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 86688b11..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
@@ -190,7 +193,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 +280,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):
@@ -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..44e3602c 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")
@@ -45,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
@@ -188,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
@@ -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"]
@@ -412,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]
@@ -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()