Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for private date (issue #61) #71

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
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
38 changes: 37 additions & 1 deletion boardgamegeek/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import logging
import sys
import warnings
import urllib.parse

from .objects.user import User
from .objects.search import SearchResult
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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``

Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions boardgamegeek/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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="",
Expand Down
19 changes: 17 additions & 2 deletions boardgamegeek/loaders/collection.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -27,14 +27,29 @@ 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,
quiet=True),
"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:
Expand Down
91 changes: 91 additions & 0 deletions boardgamegeek/objects/games.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
62 changes: 28 additions & 34 deletions boardgamegeek/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
Loading