diff --git a/boardgamegeek/api.py b/boardgamegeek/api.py index 888e34c..2747581 100644 --- a/boardgamegeek/api.py +++ b/boardgamegeek/api.py @@ -18,6 +18,7 @@ import logging import sys import warnings +import urllib.parse from .objects.user import User from .objects.search import SearchResult @@ -110,6 +111,7 @@ def __init__(self, api_endpoint, cache, timeout, retries, retry_delay, requests_ self._plays_api_url = api_endpoint + "/plays" self._hot_api_url = api_endpoint + "/hot" self._collection_api_url = api_endpoint + "/collection" + self._login_url = urllib.parse.urljoin(api_endpoint, "/login/api/v1") try: self._timeout = float(timeout) self._retries = int(retries) @@ -225,6 +227,36 @@ def guild(self, guild_id, progress=None, members=True): return guild + def log_in(self, user_name, password): + """ + Logs in to site. + + :param str user_name: user name to log-in as + :param str password: secret password for user + :retval str: error message if log-in failed. + :retval None: if log-in successful. + """ + body = {'credentials': {'username': user_name, 'password': password}} + # will set authentication cookies on self.requests_session + response = self.requests_session.post(self._login_url, json=body) + if response.status_code >= 400: + if 'application/json' == response.headers['Content-Type']: + response.cooked = response.json() + response.error = response.cooked['errors']['message'] + else: + response.error = response.text + if response.status_code >= 500: + # server error + pass + else: + # client error; likely invalid credentials + pass + return response.error + else: + return None + + sign_in = log_in + # TODO: refactor def user(self, name, progress=None, buddies=True, guilds=True, hot=True, top=True, domain=BGGRestrictDomainTo.BOARD_GAME): """ @@ -503,7 +535,7 @@ def collection(self, user_name, subtype=BGGRestrictCollectionTo.BOARD_GAME, excl version=None, own=None, rated=None, played=None, commented=None, trade=None, want=None, wishlist=None, wishlist_prio=None, preordered=None, want_to_play=None, want_to_buy=None, prev_owned=None, has_parts=None, want_parts=None, min_rating=None, rating=None, min_bgg_rating=None, bgg_rating=None, - min_plays=None, max_plays=None, collection_id=None, modified_since=None): + min_plays=None, max_plays=None, private=None, collection_id=None, modified_since=None): """ Returns an user's game collection @@ -533,6 +565,7 @@ def collection(self, user_name, subtype=BGGRestrictCollectionTo.BOARD_GAME, excl :param double rating: return items rated by the user with a maximum of ``rating`` :param double min_bgg_rating : return items rated on BGG with a minimum of ``min_bgg_rating`` :param double bgg_rating: return items rated on BGG with a maximum of ``bgg_rating`` + :param bool private: include private game info in results. Only works when viewing your own collection and you are logged in. :param int collection_id: restrict results to the collection specified by this id :param str modified_since: restrict results to those whose status (own, want, etc.) has been changed/added since ``modified_since``. Format: ``YY-MM-DD`` or ``YY-MM-DD HH:MM:SS`` @@ -626,6 +659,9 @@ def collection(self, user_name, subtype=BGGRestrictCollectionTo.BOARD_GAME, excl else: raise BGGValueError("invalid 'bgg_rating'") + if private is not None and private: + params["showprivate"] = 1 + if collection_id is not None: params["collid"] = collection_id diff --git a/boardgamegeek/cache.py b/boardgamegeek/cache.py index e6e9dbc..6158115 100644 --- a/boardgamegeek/cache.py +++ b/boardgamegeek/cache.py @@ -20,7 +20,7 @@ def __init__(self, ttl): int(ttl) except ValueError: raise BGGValueError - self.cache = requests_cache.core.CachedSession(backend="memory", expire_after=ttl, allowable_codes=(200,)) + self.cache = requests_cache.CachedSession(backend="memory", expire_after=ttl, allowable_codes=(200,)) class CacheBackendSqlite(CacheBackend): @@ -30,7 +30,7 @@ def __init__(self, path, ttl, fast_save=True): except ValueError: raise BGGValueError - self.cache = requests_cache.core.CachedSession(cache_name=path, + self.cache = requests_cache.CachedSession(cache_name=path, backend="sqlite", expire_after=ttl, extension="", diff --git a/boardgamegeek/loaders/collection.py b/boardgamegeek/loaders/collection.py index 6a40746..b3a7f60 100644 --- a/boardgamegeek/loaders/collection.py +++ b/boardgamegeek/loaders/collection.py @@ -1,7 +1,7 @@ from ..objects.collection import Collection from ..exceptions import BGGApiError, BGGItemNotFoundError from ..utils import get_board_game_version_from_element -from ..utils import xml_subelement_text, xml_subelement_attr +from ..utils import xml_attr, xml_subelement_text, xml_subelement_attr def create_collection_from_xml(xml_root, user_name): @@ -27,7 +27,7 @@ def add_collection_items_from_xml(collection, xml_root, subtype): "id": int(item.attrib["objectid"]), "image": xml_subelement_text(item, "image"), "thumbnail": xml_subelement_text(item, "thumbnail"), - "yearpublished": xml_subelement_attr(item, + "yearpublished": xml_subelement_text(item, "yearpublished", default=0, convert=int, @@ -35,6 +35,21 @@ def add_collection_items_from_xml(collection, xml_root, subtype): "numplays": xml_subelement_text(item, "numplays", convert=int, default=0), "comment": xml_subelement_text(item, "comment", default='')} + # Add private game info + private = item.find("privateinfo") + if private: + data["private"] = { + "paid": xml_attr(private, "pricepaid", convert=float, quiet=True), + "currency": xml_attr(private, "pp_currency"), + "cv_currency": xml_attr(private, "cv_currency"), + "currvalue": xml_attr(private, "currvalue", convert=float, quiet=True), + "quantity": xml_attr(private, "quantity", convert=int, quiet=True), + "acquired_on": xml_attr(private, "acquisitiondate"), + "acquired_from": xml_attr(private, "acquiredfrom"), + "location": xml_attr(private, "inventorylocation"), + "comment": xml_subelement_text(private, "privatecomment"), + } + # Add item statistics stats = item.find("stats") if stats is None: diff --git a/boardgamegeek/objects/games.py b/boardgamegeek/objects/games.py index e2eedbb..68eaf4c 100644 --- a/boardgamegeek/objects/games.py +++ b/boardgamegeek/objects/games.py @@ -58,6 +58,20 @@ def numeric_player_count(self): return int(self.player_count) +class BoardGamePrivate(DictObject): + """ + Private user info for a board game + """ + def __getattr__(self, item): + # allow accessing user's variables using .attribute + try: + return self._data[item] + except: + if item.startswith('_'): + raise AttributeError + return None + + class BoardGameStats(DictObject): """ Statistics about a board game @@ -596,6 +610,7 @@ class CollectionBoardGame(BaseGame): def __init__(self, data): super(CollectionBoardGame, self).__init__(data) + self._private = BoardGamePrivate(data.get('private', {})) def __repr__(self): return "CollectionBoardGame (id: {})".format(self.id) @@ -619,6 +634,10 @@ def _format(self, log): for v in self._versions: v._format(log) + @property + def private(self): + return self._private + @property def lastmodified(self): # TODO: deprecate this @@ -729,6 +748,78 @@ def comment(self): """ return self._data.get("comment", "") + @property + def private_comment(self): + """ + :return: private comment left by user + :rtype: str + """ + return self._private.comment + + @property + def paid(self): + """ + :return: price paid by user (private) + :rtype: str + """ + return self._private.paid + + @property + def currency(self): + """ + :return: currency for price paid by user (private) + :rtype: str + """ + return self._private.currency + + @property + def currvalue(self): + """ + :return: price paid by user (private) + :rtype: str + """ + return self._private.currvalue + + @property + def cv_currency(self): + """ + :return: currency for price paid by user (private) + :rtype: str + """ + return self._private.cv_currency + + @property + def quantity(self): + """ + :return: quantity owned by user (private) + :rtype: str + """ + return self._private.quantity + + @property + def acquired_on(self): + """ + :return: acquisition date (private) + :rtype: str + """ + return self._private.acquired_on + + @property + def acquired_from(self): + """ + :return: where game was acquired from (private) + :rtype: str + """ + return self._private.acquired_from + + @property + def location(self): + """ + :return: where game is inventoried (private) + :rtype: str + """ + return self._private.location + class BoardGame(BaseGame): """ diff --git a/boardgamegeek/utils.py b/boardgamegeek/utils.py index 49bb976..c75036f 100644 --- a/boardgamegeek/utils.py +++ b/boardgamegeek/utils.py @@ -118,6 +118,31 @@ def data(self): """ return self._data +def xml_attr(xml_elem, attribute, convert=None, default=None, quiet=False): + """ + Get a (possibly missing) attribute from an element, optionally converting it. + + :param xml_elem: element to get the attribute from + :param attribute: name of the attribute to get + :param convert: if not ``None``, a callable to perform the conversion of this attribute to a certain object type + :param default: default value if the subelement or attribute is not found + :param quiet: if ``True``, don't raise exception from conversions; return default instead + :return: value of the attribute or ``None`` in error cases + + """ + if xml_elem is None or not attribute: + return None + + value = xml_elem.attrib.get(attribute, default) + if value != default and convert: + try: + value = convert(value) + except: + if quiet: + value = default + else: + raise + return value def xml_subelement_attr_by_attr(xml_elem, subelement, filter_attr, filter_value, convert=None, attribute="value", default=None, quiet=False): """ @@ -149,18 +174,7 @@ def xml_subelement_attr_by_attr(xml_elem, subelement, filter_attr, filter_value, return None for subel in xml_elem.findall('.//{}[@{}="{}"]'.format(subelement, filter_attr, filter_value)): - value = subel.attrib.get(attribute) - if value is None: - value = default - elif convert: - try: - value = convert(value) - except: - if quiet: - value = default - else: - raise - return value + return xml_attr(subel, attribute, convert=convert, default=default, quiet=quiet) return default @@ -195,17 +209,7 @@ def xml_subelement_attr(xml_elem, subelement, convert=None, attribute="value", d if subel is None: value = default else: - value = subel.attrib.get(attribute) - if value is None: - value = default - elif convert: - try: - value = convert(value) - except: - if quiet: - value = default - else: - raise + value = xml_attr(subel, attribute, convert=convert, default=default, quiet=quiet) return value @@ -237,17 +241,7 @@ def xml_subelement_attr_list(xml_elem, subelement, convert=None, attribute="valu subel = xml_elem.findall(subelement) res = [] for e in subel: - value = e.attrib.get(attribute) - if value is None: - value = default - elif convert: - try: - value = convert(value) - except: - if quiet: - value = default - else: - raise + value = xml_attr(e, attribute, convert=convert, default=default, quiet=quiet) res.append(value) return res diff --git a/test/_common.py b/test/_common.py index 36e2265..8ec1683 100644 --- a/test/_common.py +++ b/test/_common.py @@ -6,10 +6,12 @@ import pytest import sys import re +import glob import xml.etree.ElementTree as ET from boardgamegeek import BGGClient, BGGClientLegacy, CacheBackendNone +from boardgamegeek.objects.collection import Collection # Kinda hard to test without having a "test" user @@ -65,6 +67,88 @@ def xml(): """ return ET.fromstring(xml_code) +def glob_xml_name(filepat, allowAny=False): + """ + Get the name (or raise an exception) of an XML sample in ``XML_PATH`` matching a glob pattern. + + :param str filepat: glob pattern to use for search + :param bool allowAny: If multiple files match the pattern, controls whether an exception is thrown (if ``False``) or one of the multiple files is picked & used (if ``True``). + :retval str: the pathname of a matching file + :raises FileNotFoundError: if no file matches the given pattern + :raises FileExistsError: if multiple files match the given pattern and ``allowAny`` is ``False`` + :: + """ + filenames = glob.glob(os.path.join(XML_PATH, os.path.basename(filepat))) + if 1 == len(filenames) or (filenames and allowAny): + return filenames[0] + elif filenames: + raise FileExistsError(f"Found multiple XML samples matching '{filepat}'") + else: + raise FileNotFoundError(f"No XML samples matching: '{filepat}'") + +def glob_xml_contents(filepat, allowAny=False): + """ + Get the content (or raise an exception) of an XML sample in ``XML_PATH`` matching a glob pattern. + + :param str filepat: glob pattern to use for search + :param bool allowAny: If multiple files match the pattern, controls whether an exception is thrown (if ``False``) or one of the multiple files is picked & used (if ``True``). + :retval str: the contents of a matching file + :raises FileNotFoundError: if no file matches the given pattern + :raises FileExistsError: if multiple files match the given pattern and ``allowAny`` is ``False`` + :: + """ + filename = glob_xml_name(filepat, allowAny=allowAny) + with open(filename) as xml_file: + xml = xml_file.read() + return xml + +def open_and_parse_xml(which_xml, params=None, allowAny=False): + """ + Get an XML sample and return the parsed result. + + :param str which_xml: partial URL endpoint or glob pattern for the XML sample + :param list params: query parameters (only if which_xml is an endpoint) + :param bool allowAny: If multiple files match the pattern, controls whether an exception is thrown (if ``False``) or one of the multiple files is picked & used (if ``True``). + :retval xml.etree.ElementTree.Element: the contents of a matching file, parsed as XML + :raises FileNotFoundError: if no file matches the given pattern + :raises FileExistsError: if multiple files match the given pattern and ``allowAny`` is ``False`` + """ + if params is not None: + response = simulate_bgg("/" + which_xml, params, timeout=-1) + xml = response.text + else: + xml = glob_xml_contents(which_xml, allowAny=allowAny) + + if sys.version_info >= (3,): + return ET.fromstring(xml) + else: + return ET.fromstring(xml.encode("utf-8")) + + + +@pytest.fixture +def xml_collection_minimal(): + return open_and_parse_xml("collection@*&which=minimal") + +@pytest.fixture +def xml_collection_brief(): + return open_and_parse_xml("collection@*&which=brief") + +@pytest.fixture +def xml_collection_error(): + return open_and_parse_xml("collection", { + "username": TEST_INVALID_USER, + "subtype":"boardgame", "stats":1, + }) + +@pytest.fixture +def xml_collection_full(): + return open_and_parse_xml("collection@*&which=full") + +@pytest.fixture +def xml_collection_without_stats(): + return open_and_parse_xml("collection@*&which=nostats", allowAny=True) + @pytest.fixture def bgg(): diff --git a/test/test_collection.py b/test/test_collection.py index 7befcec..89e48a5 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -5,7 +5,7 @@ from _common import * from boardgamegeek import BGGError, BGGValueError, BGGItemNotFoundError from boardgamegeek.objects.collection import CollectionBoardGame, Collection -from boardgamegeek.objects.games import BoardGameVersion +from boardgamegeek.objects.games import BoardGameVersion, BoardGamePrivate import time @@ -63,28 +63,39 @@ def test_creating_collection_out_of_raw_data(): Collection({"items": [{"id": 102}]}) # test that items are added to the collection from the constructor - c = Collection({"owner": "me", - "items": [{"id": 100, - "name": "foobar", - "image": "", - "thumbnail": "", - "yearpublished": 1900, - "numplays": 32, - "comment": "This game is great!", - "minplayers": 1, - "maxplayers": 5, - "minplaytime": 60, - "maxplaytime": 120, - "playingtime": 100, - "stats": { - "usersrated": 123, - "ranks": [{ - "id": "1", "type": "subtype", "name": "boardgame", "friendlyname": "friendly", - "value": "10", "bayesaverage": "0.51" - }] - } - - }]}) + collection_data = { + "owner": "me", + "items": [{ + "id": 100, + "name": "foobar", + "image": "", + "thumbnail": "", + "yearpublished": 1900, + "numplays": 32, + "comment": "This game is great!", + "minplayers": 1, + "maxplayers": 5, + "minplaytime": 60, + "maxplaytime": 120, + "playingtime": 100, + "stats": { + "usersrated": 123, + "ranks": [{ + "id": "1", "type": "subtype", "name": "boardgame", "friendlyname": "friendly", + "value": "10", "bayesaverage": "0.51" + }] + }, + "private": { + "comment": "private comment", + "paid":42.0, "currency":"USD", + "currvalue": 23.0, "cv_currency":"EUR", + "quantity":"1", + "acquired_on": "2000-01-01", + "acquired_from": "store", + "location": "home", + }, + }]} + c = Collection(collection_data) assert len(c) == 1 assert c.owner == "me" @@ -106,6 +117,44 @@ def test_creating_collection_out_of_raw_data(): assert ci.users_rated == 123 assert ci.rating_bayes_average is None + private_data = collection_data['items'][0]['private'] + assert ci.private is not None + assert ci.private_comment == private_data['comment'] + assert ci.paid == private_data['paid'] + assert ci.currency == private_data['currency'] + assert ci.currvalue == private_data["currvalue"] + assert ci.cv_currency == private_data["cv_currency"] + assert ci.quantity == private_data["quantity"] + assert ci.acquired_on == private_data["acquired_on"] + assert ci.acquired_from == private_data["acquired_from"] + assert ci.location == private_data["location"] + with pytest.raises(BGGError): # raises exception on invalid game data c.add_game({"bla": "bla"}) + +def test_creating_private_out_of_raw_data(): + # pre + private_data = { + "comment": "private comment", + "paid": 42.0, "currency": "USD", + "currvalue": 23.0, "cv_currency":"EUR", + "quantity":"1", + "acquired_on": "2000-01-01", + "acquired_from": "store", + "location": "home", + } + + # in + prvt = BoardGamePrivate(private_data) + + # post + assert prvt.paid == private_data['paid'] + assert prvt.currency == private_data['currency'] + assert prvt.comment == private_data['comment'] + assert prvt.quantity == private_data['quantity'] + assert prvt.currvalue == private_data["currvalue"] + assert prvt.cv_currency == private_data["cv_currency"] + assert prvt.acquired_on == private_data["acquired_on"] + assert prvt.acquired_from == private_data["acquired_from"] + assert prvt.location == private_data["location"] diff --git a/test/test_loaders_collection.py b/test/test_loaders_collection.py new file mode 100644 index 0000000..9ac4ec0 --- /dev/null +++ b/test/test_loaders_collection.py @@ -0,0 +1,164 @@ +from __future__ import unicode_literals + +import pytest +import mock + +from _common import * +from boardgamegeek.exceptions import BGGApiError, BGGItemNotFoundError +from boardgamegeek.objects.collection import Collection +from boardgamegeek.loaders.collection import create_collection_from_xml, add_collection_items_from_xml + + +MISSING_STR_VAL = "missing" +MISSING_INT_VAL = -1 +MISSING_FLOAT_VAL = -10. + + +def test_create_collection_from_xml_error(xml_collection_error): + with pytest.raises(BGGItemNotFoundError, match=re.escape(xml_collection_error.findtext("*/message", default=MISSING_STR_VAL))): + create_collection_from_xml(xml_collection_error, TEST_INVALID_USER) + + +def test_create_collection_from_xml_minimal(xml_collection_minimal): + # in + collection = create_collection_from_xml(xml_collection_minimal, TEST_VALID_USER) + + # post + assert collection.owner == TEST_VALID_USER + + + +def test_add_collection_items_from_xml_without_stats(xml_collection_without_stats, mocker): + # pre + with pytest.raises(BGGApiError, match="missing 'stats'"): + collection = mocker.MagicMock(Collection) + + # in + add_collection_items_from_xml(collection, xml_collection_without_stats, "boardgame") + + +def test_add_collection_items_from_xml_minimal(xml_collection_minimal, mocker): + # pre + collection = mocker.MagicMock(Collection) + + # in + add_collection_items_from_xml(collection, xml_collection_minimal, "boardgame") + + # post + collection.add_game.assert_called() + actual = collection.add_game.call_args.args[0] + assert actual is not None + + item = xml_collection_minimal.find("item[@subtype='boardgame']") + expected = { + "id": int(item.attrib.get("objectid", MISSING_INT_VAL)), + "comment": "", + "name":None, "image":None, "thumbnail":None, "rating":None, + "yearpublished":0, "numplays":0, "minplayers":0, "maxplayers":0, + "minplaytime":0, "maxplaytime":0, "playingtime":0, + "stats": { + "usersrated":None, "average":None, "bayesaverage":None, + "stddev":None, "median":None, + "ranks":[], + } + } + assert actual == expected + + +def test_add_collection_items_from_xml_brief(xml_collection_brief, mocker): + # pre + collection = mocker.MagicMock(Collection) + + # in + add_collection_items_from_xml(collection, xml_collection_brief, "boardgame") + + # post + collection.add_game.assert_called() + actual = collection.add_game.call_args.args[0] + assert actual is not None + + item = xml_collection_brief.find("item[@subtype='boardgame']") + stats = item.find("stats") + expected = { + "id": int(item.attrib.get("objectid", MISSING_INT_VAL)), + "name": item.findtext("name", default=MISSING_STR_VAL), + "comment": "", + "image":None, "thumbnail":None, + "yearpublished":0, "numplays":0, + "stats": { + "usersrated":None, "average":None, "bayesaverage":None, + "stddev":None, "median":None, + "ranks":[], + }, + "rating": float(stats.find("rating").attrib.get("value", MISSING_FLOAT_VAL)), + } + + del stats.attrib["numowned"] + for key, value in stats.attrib.items(): + expected[key] = int(value) + status = item.find("status") + for key, value in status.attrib.items(): + expected[key] = value + + assert actual == expected + + +def test_add_collection_items_from_xml_full(xml_collection_full, mocker): + # pre + collection = mocker.MagicMock(Collection) + + # in + add_collection_items_from_xml(collection, xml_collection_full, "boardgame") + + # post + collection.add_game.assert_called() + actual = collection.add_game.call_args.args[0] + assert actual is not None + + item = xml_collection_full.find("item[@subtype='boardgame']") + stats = item.find("stats") + private = item.find("privateinfo") + expected = { + "id": int(item.attrib.get("objectid", MISSING_INT_VAL)), + "name": item.findtext("name", default=MISSING_STR_VAL), + "comment": item.findtext("comment", default=MISSING_STR_VAL), + "image": item.findtext("image", default=MISSING_STR_VAL), + "thumbnail": item.findtext("thumbnail", default=MISSING_STR_VAL), + "yearpublished": int(item.findtext("yearpublished", default=MISSING_INT_VAL)), + "numplays": int(item.findtext("numplays", default=MISSING_INT_VAL)), + "stats": { + "usersrated":None, "average":None, "bayesaverage":None, + "stddev":None, "median":None, + "ranks":[], + }, + "rating": float(stats.find("rating").attrib.get("value", MISSING_FLOAT_VAL)), + "private": { + "comment": private.findtext("privatecomment", default=MISSING_STR_VAL), + "paid": float(private.attrib.get("pricepaid", MISSING_FLOAT_VAL)), + "currency": private.attrib.get("pp_currency", MISSING_STR_VAL), + "currvalue": float(private.attrib.get("currvalue", MISSING_FLOAT_VAL)), + "cv_currency": private.attrib.get("cv_currency", MISSING_STR_VAL), + "quantity": int(private.attrib.get("quantity", MISSING_INT_VAL)), + "acquired_on": private.attrib.get("acquisitiondate", MISSING_STR_VAL), + "acquired_from": private.attrib.get("acquiredfrom", MISSING_STR_VAL), + "location": private.attrib.get("inventorylocation", MISSING_STR_VAL), + } + } + + del stats.attrib["numowned"] + for key, value in stats.attrib.items(): + expected[key] = int(value) + for rank in stats.findall("ranks/rank"): + expected["stats"]["ranks"].append({ + "type": rank.attrib.get("type", MISSING_STR_VAL), + "id": rank.attrib.get("id", MISSING_INT_VAL), + "name": rank.attrib.get("name", MISSING_STR_VAL), + "friendlyname": rank.attrib.get("friendlyname", MISSING_STR_VAL), + "value": rank.attrib.get("value", MISSING_STR_VAL), + "bayesaverage": float(rank.attrib.get("bayesaverage", MISSING_FLOAT_VAL)), + }) + status = item.find("status") + for key, value in status.attrib.items(): + expected[key] = value + + assert actual == expected diff --git a/test/test_utils.py b/test/test_utils.py index d358c55..9f4f059 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -7,6 +7,47 @@ from _common import * from boardgamegeek.objects.things import Thing +def test_get_xml_attr(xml): + + node = bggutil.xml_attr(None, "attr") + assert node is None + + node = bggutil.xml_attr(None, "attr", default='default') + assert node is None + + node = bggutil.xml_attr(xml, None) + assert node is None + + node = bggutil.xml_attr(xml, None, default='default') + assert node is None + + node = bggutil.xml_attr(xml, "") + assert node is None + + node = bggutil.xml_attr(xml.find("node1"), "attr") + assert node == "hello1" + + node = bggutil.xml_attr(xml.find("node1"), "int_attr", convert=int) + assert node == 1 + + # test that default works + node = bggutil.xml_attr(xml.find("node_thats_missing"), "attr", default="default") + assert node == None + + node = bggutil.xml_attr(xml.find("node1"), "attribute_thats_missing", default=1234) + assert node == 1234 + + # test quiet + with pytest.raises(Exception): + # attr can't be converted to int + node = bggutil.xml_attr(xml.find("node1"), "attr", convert=int) + + node = bggutil.xml_attr(xml.find("node1"), "attr", convert=int, quiet=True) + assert node == None + + node = bggutil.xml_attr(xml.find("node1"), "attr", convert=int, default=999, quiet=True) + assert node == 999 + def test_get_xml_subelement_attr(xml): node = bggutil.xml_subelement_attr(None, "hello") diff --git a/test/xml/collection@brief=1&id=34219&stats=1&username=fagentu007&which=brief b/test/xml/collection@brief=1&id=34219&stats=1&username=fagentu007&which=brief new file mode 100644 index 0000000..25d8d60 --- /dev/null +++ b/test/xml/collection@brief=1&id=34219&stats=1&username=fagentu007&which=brief @@ -0,0 +1,13 @@ + + + + Biblios + + + + + + + + + diff --git a/test/xml/collection@id=34219&private=1&stats=1&username=fagentu007&version=1&which=full b/test/xml/collection@id=34219&private=1&stats=1&username=fagentu007&version=1&which=full new file mode 100644 index 0000000..c9e69a9 --- /dev/null +++ b/test/xml/collection@id=34219&private=1&stats=1&username=fagentu007&version=1&which=full @@ -0,0 +1,28 @@ + + + + Biblios + 2007 + https://cf.geekdo-images.com/images/pic759154.jpg + https://cf.geekdo-images.com/images/pic759154_t.jpg + + + + + + + + + + + + + + + Private comment. + + Public comment. + + 5 + + diff --git a/test/xml/collection@id=34219&stats=0&username=fagentu007&version=1&which=nostats b/test/xml/collection@id=34219&stats=0&username=fagentu007&version=1&which=nostats new file mode 100644 index 0000000..ae07cde --- /dev/null +++ b/test/xml/collection@id=34219&stats=0&username=fagentu007&version=1&which=nostats @@ -0,0 +1,11 @@ + + + + Biblios + 2007 + https://cf.geekdo-images.com/images/pic759154.jpg + https://cf.geekdo-images.com/images/pic759154_t.jpg + + 0 + + diff --git a/test/xml/collection@id=34219&stats=1&username=fagentu007&which=minimal b/test/xml/collection@id=34219&stats=1&username=fagentu007&which=minimal new file mode 100644 index 0000000..bae9a7d --- /dev/null +++ b/test/xml/collection@id=34219&stats=1&username=fagentu007&which=minimal @@ -0,0 +1,7 @@ + + + + + + +