Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Changelog
Due to this library relying on external content, older versions are not guaranteed to work.
Try to always use the latest version.

.. _v2.1.0:

2.1.0 (2019-06-17)
==================

- Added ways to sort and filter House list results like in Tibia.com.
- Added support to get the Boosted Creature of the day.

.. _v2.0.1:

2.0.1 (2019-06-04)
Expand Down
22 changes: 19 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ Enumerations
============
Enumerations are provided for various values in order to avoid depending on strings.

.. autoclass:: HouseType
:members:
:undoc-members:

.. autoclass:: AccountStatus
:members:
Expand All @@ -30,10 +27,18 @@ Enumerations are provided for various values in order to avoid depending on stri
:members:
:undoc-members:

.. autoclass:: HouseOrder
:members:
:undoc-members:

.. autoclass:: HouseStatus
:members:
:undoc-members:

.. autoclass:: HouseType
:members:
:undoc-members:

.. autoclass:: NewsCategory
:members:
:undoc-members:
Expand All @@ -50,6 +55,11 @@ Enumerations are provided for various values in order to avoid depending on stri
:members:
:undoc-members:

.. autoclass:: TournamentWorldType
:members:
:undoc-members:


.. autoclass:: TransferType
:members:
:undoc-members:
Expand All @@ -73,6 +83,12 @@ Main Models
The following models all contain their respective ``from_content`` methods.
They all have their respective section in Tibia.com

BoostedCreature
---------------
.. autoclass:: BoostedCreature
:members:
:inherited-members:

Character
---------
.. autoclass:: Character
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion tests/tests_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from tests.tests_news import FILE_NEWS_LIST, FILE_NEWS_ARTICLE
from tests.tests_tibiapy import TestCommons
from tibiapy import Client, Character, Guild, Highscores, VocationFilter, Category, House, ListedHouse, ListedGuild, \
KillStatistics, ListedNews, News, World, WorldOverview, Forbidden, NetworkError
KillStatistics, ListedNews, News, World, WorldOverview, Forbidden, NetworkError, BoostedCreature


class TestClient(asynctest.TestCase, TestCommons):
Expand Down Expand Up @@ -168,7 +168,13 @@ async def testFetchWorldList(self, mock):

self.assertIsInstance(worlds, WorldOverview)

@aioresponses()
async def testFetchBoostedCreature(self, mock):
content = self._load_resource(self.FILE_UNRELATED_SECTION)
mock.get(News.get_list_url(), status=200, body=content)
creature = await self.client.fetch_boosted_creature()

self.assertIsInstance(creature, BoostedCreature)



20 changes: 20 additions & 0 deletions tests/tests_creature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import unittest

from tests.tests_tibiapy import TestCommons
from tibiapy import BoostedCreature, InvalidContent


class TestCreature(TestCommons, unittest.TestCase):
# region Tibia.com Tests
def testBoostedCreature(self):
content = self._load_resource(self.FILE_UNRELATED_SECTION)
creature = BoostedCreature.from_content(content)

self.assertIsInstance(creature, BoostedCreature)
self.assertEqual("Skeleton Warrior", creature.name)

def testBoostedCreatureNotTibiaCom(self):
with self.assertRaises(InvalidContent):
BoostedCreature.from_content("<html><div><p>Nothing</p></div></html>")

# endregion
3 changes: 3 additions & 0 deletions tests/tests_highscores.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ def testHighscoresTibiaData(self):
self.assertEqual(highscores.category, Category.AXE_FIGHTING)
self.assertEqual(highscores.results_count, 300)

self.assertEqual(highscores.url_tibiadata,
Highscores.get_url_tibiadata(highscores.world, highscores.category, highscores.vocation))

for entry in highscores.entries:
self.assertIsInstance(entry, HighscoresEntry)
self.assertIsInstance(entry.name, str)
Expand Down
17 changes: 17 additions & 0 deletions tests/tests_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from tests.tests_tibiapy import TestCommons
from tibiapy import enums, utils
from tibiapy.utils import parse_integer, parse_tibia_money

TIBIA_DATETIME_CEST = "Jul 10 2018, 07:13:32 CEST"
TIBIA_DATETIME_CET = "Jan 10 2018, 07:13:32 CET"
Expand Down Expand Up @@ -145,6 +146,7 @@ def testTryDateTime(self):

def testParseNumberWords(self):
self.assertEqual(utils.parse_number_words("one"), 1)
self.assertEqual(utils.parse_number_words("no"), 0)
self.assertEqual(utils.parse_number_words("..."), 0)
self.assertEqual(utils.parse_number_words("twenty-one"), 21)
self.assertEqual(utils.parse_number_words("one hundred two"), 102)
Expand All @@ -160,3 +162,18 @@ def testEnumStr(self):
self.assertEqual(enums.VocationFilter.from_name("royal paladin"), enums.VocationFilter.PALADINS)
self.assertEqual(enums.VocationFilter.from_name("unknown"), enums.VocationFilter.ALL)
self.assertIsNone(enums.VocationFilter.from_name("unknown", False))

def testParseTibiaMoney(self):
self.assertEqual(1000, parse_tibia_money("1k"))
self.assertEqual(5000000, parse_tibia_money("5kk"))
self.assertEqual(2500, parse_tibia_money("2.5k"))
self.assertEqual(50, parse_tibia_money("50"))
with self.assertRaises(ValueError):
parse_tibia_money("abc")

def testParseInteger(self):
self.assertEqual(1450, parse_integer("1.450"))
self.assertEqual(1110, parse_integer("1,110"))
self.assertEqual(15, parse_integer("15"))
self.assertEqual(0, parse_integer("abc"))
self.assertEqual(-1, parse_integer("abc", -1))
3 changes: 3 additions & 0 deletions tests/tests_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ def testWorldOverview(self):
self.assertGreater(world_overview.total_online, 0)
self.assertIsNotNone(world_overview.record_date)
self.assertIsNotNone(world_overview.record_count)
self.assertEqual(len(world_overview.regular_worlds), 65)
self.assertEqual(len(world_overview.tournament_worlds), 6)

worlds = ListedWorld.list_from_content(content)
self.assertEqual(len(world_overview.worlds), len(worlds))
Expand Down Expand Up @@ -206,6 +208,7 @@ def testWorldOverviewTibiaData(self):

self.assertIsInstance(world_overview, WorldOverview)
self.assertEqual(WorldOverview.get_url(), ListedWorld.get_list_url())
self.assertEqual(WorldOverview.get_url_tibiadata(), ListedWorld.get_list_url_tibiadata())
self.assertGreater(sum(w.online_count for w in world_overview.worlds), 0)
self.assertIsInstance(world_overview.worlds[0], ListedWorld)
self.assertIsInstance(world_overview.worlds[0].pvp_type, PvpType)
Expand Down
3 changes: 2 additions & 1 deletion tibiapy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
from tibiapy.kill_statistics import *
from tibiapy.news import *
from tibiapy.world import *
from tibiapy.creature import *
from tibiapy.client import *

__version__ = '2.0.1'
__version__ = '2.1.0'

from logging import NullHandler

Expand Down
14 changes: 10 additions & 4 deletions tibiapy/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from collections import OrderedDict
from enum import Enum

from tibiapy.enums import HouseType
from tibiapy.enums import HouseType, HouseStatus, HouseOrder

CHARACTER_URL = "https://www.tibia.com/community/?subtopic=characters&name=%s"
CHARACTER_URL_TIBIADATA = "https://api.tibiadata.com/v2/characters/%s.json"
HOUSE_URL = "https://www.tibia.com/community/?subtopic=houses&page=view&houseid=%d&world=%s"
HOUSE_URL_TIBIADATA = "https://api.tibiadata.com/v2/house/%s/%d.json"
HOUSE_LIST_URL = "https://www.tibia.com/community/?subtopic=houses&world=%s&town=%s&type=%s"
HOUSE_LIST_URL = "https://www.tibia.com/community/?subtopic=houses&world=%s&town=%s&type=%s&status=%s&order=%s"
HOUSE_LIST_URL_TIBIADATA = "https://api.tibiadata.com/v2/houses/%s/%s/%s.json"
GUILD_URL = "https://www.tibia.com/community/?subtopic=guilds&page=view&GuildName=%s"
GUILD_URL_TIBIADATA = "https://api.tibiadata.com/v2/guild/%s.json"
Expand Down Expand Up @@ -321,7 +321,8 @@ def get_url_tibiadata(cls, house_id, world):
return HOUSE_URL_TIBIADATA % (world, house_id)

@classmethod
def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE):
def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE, status: HouseStatus = None,
order=HouseOrder.NAME):
"""
Gets the URL to the house list on Tibia.com with the specified parameters.

Expand All @@ -333,14 +334,19 @@ def get_list_url(cls, world, town, house_type: HouseType = HouseType.HOUSE):
The name of the town.
house_type: :class:`HouseType`
Whether to search for houses or guildhalls.
status: :class:`HouseStatus`, optional
The house status to filter results. By default no filters will be applied.
order: :class:`HouseOrder`, optional
The ordering to use for the results. By default they are sorted by name.

Returns
-------
:class:`str`
The URL to the list matching the parameters.
"""
house_type = "%ss" % house_type.value
return HOUSE_LIST_URL % (urllib.parse.quote(world), urllib.parse.quote(town), house_type)
status = "" if status is None else status.value
return HOUSE_LIST_URL % (urllib.parse.quote(world), urllib.parse.quote(town), house_type, status, order.value)

@classmethod
def get_list_url_tibiadata(cls, world, town, house_type: HouseType = HouseType.HOUSE):
Expand Down
43 changes: 37 additions & 6 deletions tibiapy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import tibiapy
from tibiapy import Character, Guild, World, House, KillStatistics, ListedGuild, Highscores, Category, VocationFilter, \
ListedHouse, HouseType, WorldOverview, NewsCategory, NewsType, ListedNews, News, Forbidden, NetworkError
ListedHouse, HouseType, WorldOverview, NewsCategory, NewsType, ListedNews, News, Forbidden, NetworkError, \
HouseStatus, HouseOrder, BoostedCreature

__all__ = (
"Client",
Expand Down Expand Up @@ -37,11 +38,14 @@ def __init__(self, loop=None, session=None):
self.loop.create_task(self._initialize_session())

async def _initialize_session(self):
self.session = aiohttp.ClientSession(loop=self.loop) # type: aiohttp.ClientSession
headers = {
'User-Agent ': "Tibia.py/%s (+https://github.com/Galarzaa90/tibia.py" % tibiapy.__version__
}
self.session = aiohttp.ClientSession(loop=self.loop, headers=headers) # type: aiohttp.ClientSession

@classmethod
def _handle_status(cls, status_code):
"""Handles error status codes, raising exceptions if neccesary."""
"""Handles error status codes, raising exceptions if necessary."""
if status_code < 400:
return
if status_code == 403:
Expand Down Expand Up @@ -99,6 +103,28 @@ async def _post(self, url, data):
except aiohttp.ClientError as e:
raise NetworkError("aiohttp.ClientError: %s" % e, e)

async def fetch_boosted_creature(self):
"""Fetches today's boosted creature.

.. versionadded:: 2.1.0

Returns
-------
:class:`BoostedCreature`
The boosted creature of the day.

Raises
------
Forbidden
If a 403 Forbidden error was returned.
This usually means that Tibia.com is rate-limiting the client because of too many requests.
NetworkError
If there's any connection errors during the request.
"""
content = await self._get(News.get_list_url())
boosted_creature = BoostedCreature.from_content(content)
return boosted_creature

async def fetch_character(self, name):
"""Fetches a character by its name from Tibia.com

Expand Down Expand Up @@ -258,7 +284,8 @@ async def fetch_world(self, name):
world = World.from_content(content)
return world

async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE):
async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE, status: HouseStatus = None,
order=HouseOrder.NAME):
"""Fetches the house list of a world and type.

Parameters
Expand All @@ -269,6 +296,10 @@ async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE):
The name of the town.
house_type: :class:`HouseType`
The type of building. House by default.
status: :class:`HouseStatus`, optional
The house status to filter results. By default no filters will be applied.
order: :class:`HouseOrder`, optional
The ordering to use for the results. By default they are sorted by name.

Returns
-------
Expand All @@ -283,15 +314,15 @@ async def fetch_world_houses(self, world, town, house_type=HouseType.HOUSE):
NetworkError
If there's any connection errors during the request.
"""
content = await self._get(ListedHouse.get_list_url(world, town, house_type))
content = await self._get(ListedHouse.get_list_url(world, town, house_type, status, order))
houses = ListedHouse.list_from_content(content)
return houses

async def fetch_world_guilds(self, world: str):
"""Fetches the list of guilds in a world from Tibia.com

Parameters
----------
---------cl-
world: :class:`str`
The name of the world.

Expand Down
67 changes: 67 additions & 0 deletions tibiapy/creature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import bs4

from tibiapy import abc, InvalidContent

__all__ = (
"BoostedCreature",
)

BOOSTED_ALT = "Today's boosted creature: "


class BoostedCreature(abc.Serializable):
"""Represents a boosted creature entry.

This creature changes every server save and applies to all Game Worlds.
Boosted creatures yield twice the amount of experience points, carry more loot and respawn at a faster rate.

Attributes
----------
name: :class:`str`
The name of the boosted creature.
image_url: :class:`str`
An URL containing the boosted creature's image.
"""
__slots__ = (
"name",
"image_url",
)

def __init__(self, name, image_url):
self.name = name
self.image_url = image_url

def __repr__(self):
return "<{0.__class__.__name__} name={0.name!r} image_url={0.image_url!r}>".format(self)

@classmethod
def from_content(cls, content):
"""
Gets the boosted creature from any Tibia.com page.


Parameters
----------
content: :class:`str`
The HTML content of a Tibia.com page.

Returns
-------
:class:`News`
The boosted article shown.

Raises
------
InvalidContent
If content is not the HTML of a Tibia.com's page.
"""
try:
parsed_content = bs4.BeautifulSoup(content.replace('ISO-8859-1', 'utf-8'), "lxml",
parse_only=bs4.SoupStrainer("div", attrs={"id": "RightArtwork"}))
img = parsed_content.find("img", attrs={"id": "Monster"})
name = img["title"].replace(BOOSTED_ALT, "").strip()
image_url = img["src"]
return cls(name, image_url)
except TypeError:
raise InvalidContent("content is not from Tibia.com")

Loading