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 @@ +
+
Highscores Filter
World:
Vocation:
Category:

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

+ \ No newline at end of file diff --git a/tests/resources/highscores/tibiacom_experience.txt b/tests/resources/highscores/tibiacom_experience.txt new file mode 100644 index 00000000..114b19ad --- /dev/null +++ b/tests/resources/highscores/tibiacom_experience.txt @@ -0,0 +1,4 @@ +
+
Highscores Filter
World:
Vocation:
Category:

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

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

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

Highscores
RankNameVocationLevel
76Przeokrutny KnightElite Knight11
77Mighty BorisKnight11
78DiasterusElite Knight11
79Werdix LegendElite Knight11
80Pato KinguElite Knight11
81Swetty BellaElite Knight11
82RyllandKnight11
83TazeedKnight11
84Hobab Da TankerElite Knight11
85Ariku KendElite Knight11
86BellegarrElite Knight11
87OdynieckElite Knight11
88XeniaElite Knight11
89KabelganoElite Knight11
90Elite HamattoElite Knight11
91LosoKnight11
92Steel rastalElite Knight11
93AktarielKnight11
94Soviet QueenKnight11
95Lidera OlaaElite Knight11
96AxmenaElite Knight11
97Goblin EonElite Knight11
98Vitao vidalokaElite Knight11
99Raphael MasterfulElite Knight11
100BoolecElite Knight11
» Pages: 1 2 3 4 5 6 7 8 9 10 11 12
» Results: 300
+ + \ No newline at end of file diff --git a/tests/resources/highscores/tibiacom_loyalty.txt b/tests/resources/highscores/tibiacom_loyalty.txt new file mode 100644 index 00000000..881262ba --- /dev/null +++ b/tests/resources/highscores/tibiacom_loyalty.txt @@ -0,0 +1,3 @@ +
+
Highscores Filter
World:
Vocation:
Category:

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

Highscores
RankNameVocationTitlePoints
1ScrumdiliumpiousRoyal PaladinSage of Tibia5,608
2DawoxRoyal PaladinSage of Tibia5,227
3Alexzander The GreatRoyal PaladinSage of Tibia5,110
4TaziseRoyal PaladinSage of Tibia5,108
5MickeysRoyal PaladinSage of Tibia5,011
6La FargePaladinGuardian of Tibia4,929
7Maria TerezaPaladinGuardian of Tibia4,839
8Ninja BetaRoyal PaladinGuardian of Tibia4,684
9AzazielRoyal PaladinGuardian of Tibia4,554
10Sara NithPaladinGuardian of Tibia4,465
11TokfiaRoyal PaladinGuardian of Tibia4,449
12Chrome CougarRoyal PaladinGuardian of Tibia4,324
13SejalzorRoyal PaladinGuardian of Tibia4,202
14Sir DzidekRoyal PaladinGuardian of Tibia4,183
15Kaly ShieRoyal PaladinGuardian of Tibia4,119
16Amber WolfhavenPaladinGuardian of Tibia4,097
17MauslayerRoyal PaladinGuardian of Tibia4,056
18Yathavek SapervarusPaladinKeeper of Tibia3,973
19Loyal Paladin Of CalmeraRoyal PaladinKeeper of Tibia3,948
20Shanix PoxaPaladinKeeper of Tibia3,922
21Neon HeroPaladinKeeper of Tibia3,886
22CantinelleRoyal PaladinKeeper of Tibia3,885
23Greens ArrowRoyal PaladinKeeper of Tibia3,853
24RawmanPaladinKeeper of Tibia3,765
25RocktronRoyal PaladinKeeper of Tibia3,631
» Pages: 1 2 3
» Results: 65
+ \ No newline at end of file diff --git a/tests/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()