Source code for okcupyd.profile

import datetime
import logging

from lxml import html
import simplejson

from . import details
from . import essay
from . import helpers
from . import looking_for
from . import util
from .question import QuestionFetcher
from .xpath import xpb


log = logging.getLogger(__name__)


[docs]class Profile(object): """Represent the profile of an okcupid user. Many of the attributes on this object are :class:`~okcupyd.util.cached_property` instances which lazily load their values, and cache them once they have been accessed. This makes it so that this object avoids making unnecessary HTTP requests to retrieve the same piece of information twice. Because of this caching behavior, care must be taken to invalidate cached attributes on the object if an up to date view of the profile is needed. It is recommended that you call :meth:`.refresh` to accomplish this, but it is also possible to use :meth:`~okcupyd.util.cached_property.bust_self` to bust individual properties if necessary. """ def __init__(self, session, username, **kwargs): """ :param session: A logged in :class:`~okcupyd.session.Session` :param username: The username associated with the profile. """ self._session = session #: The username of the user to whom this profile belongs. self.username = username #: A :class:`~okcupyd.util.fetchable.Fetchable` of #: :class:`~okcupyd.question.Question` instances, each corresponding #: to a question that has been answered by the user to whom this #: profile belongs. #: The fetchable consists of :class:`~okcupyd.question.UserQuestion` #: instead when the profile belongs to the logged in user. self.questions = self.question_fetchable() #: A :class:`~okcupyd.details.Details` instance belonging to the same #: user that this profile belongs to. self.details = details.Details(self) if kwargs: self._set_cached_properties(kwargs) def _set_cached_properties(self, values): property_names = set( name for name, _ in util.cached_property.get_cached_properties(self) ) for key, value in values.items(): if key not in property_names: log.warning("Unrecognized kwarg {0} with value {1} " "passed to Profile constructor.") self.__dict__[key] = value
[docs] def refresh(self, reload=False): """ :param reload: Make the request to return a new profile tree. This will result in the caching of the profile_tree attribute. The new profile_tree will be returned. """ util.cached_property.bust_caches(self, excludes=('authcode')) self.questions = self.question_fetchable() if reload: return self.profile_tree
@property def is_logged_in_user(self): """ :returns: `True` if this profile and the session it was created with belong to the same user and False otherwise.""" return self._session.log_in_name.lower() == self.username.lower() @util.cached_property def _profile_response(self): return self._session.okc_get( u'profile/{0}'.format(self.username) ).content @util.cached_property def profile_tree(self): """ :returns: a :class:`lxml.etree` created from the html of the profile page of the account associated with the username that this profile was insantiated with. """ return html.fromstring(self._profile_response)
[docs] def message_request_parameters(self, content, thread_id): return { 'ajax': 1, 'sendmsg': 1, 'r1': self.username, 'body': content, 'threadid': thread_id, 'authcode': self.authcode, 'reply': 1 if thread_id else 0, 'from_profile': 1 }
@util.cached_property def authcode(self): return helpers.get_authcode(self.profile_tree) _photo_info_xpb = xpb.div.with_class('photo').img.select_attribute_('src') @util.cached_property def photo_infos(self): """ :returns: list of :class:`~okcupyd.photo.Info` instances for each photo displayed on okcupid. """ from . import photo pics_request = self._session.okc_get( u'profile/{0}/album/0'.format(self.username), ) pics_tree = html.fromstring(u'{0}{1}{2}'.format( u'<div>', pics_request.json()['fulls'], u'</div>' )) return [photo.Info.from_cdn_uri(uri) for uri in self._photo_info_xpb.apply_(pics_tree)] @util.cached_property def looking_for(self): """ :returns: A :class:`~okcupyd.looking_for.LookingFor` instance associated with this profile. """ return looking_for.LookingFor(self) _liked_xpb = xpb.button.with_class('binary_rating_button') @property def rating(self): """ Deprecated. Use :meth:`.liked` instead. :returns: the rating that the logged in user has given this user or 0 if no rating has been given. """ return 5 if self.liked else 0 @util.cached_property def liked(self): """ :returns: Whether or not the logged in user liked this profile """ if self.is_logged_in_user: return False classes = self._liked_xpb.one_(self.profile_tree).attrib['class'].split() return 'liked' in classes _contacted_xpb = xpb.div.with_class('actions2015').button.\ with_classes('actions2015-chat', 'flatbutton', 'blue').\ select_attribute_('data-tooltip') @util.cached_property def contacted(self): """ :retuns: A boolean indicating whether the logged in user has contacted the owner of this profile. """ try: contacted_span = self._contacted_xpb.one_(self.profile_tree) except: return False else: timestamp = contacted_span.replace('Last contacted ', '') return helpers.parse_date_updated(timestamp) @util.cached_property def responds(self): """ :returns: The frequency with which the user associated with this profile responds to messages. """ contacted_text = self._contacted_xpb.\ get_text_(self.profile_tree).lower() if 'contacted' not in contacted_text: return contacted_text.strip().replace('replies ', '') _id_xpb = xpb.button.with_class('binary_rating_button').\ select_attribute_("data-tuid") @util.cached_property def id(self): """ :returns: The id that okcupid.com associates with this profile. """ if self.is_logged_in_user: return self._current_user_id return int(self._id_xpb.one_(self.profile_tree)) @util.cached_property def _current_user_id(self): return int(helpers.get_id(self.profile_tree)) @util.cached_property def essays(self): """ :returns: A :class:`~okcupyd.essay.Essays` instance that is associated with this profile. """ return essay.Essays(self) _age_xpb = xpb.span.with_class('userinfo2015-basics-asl-age') _user_age_xpb = xpb.span(id='ajax_age') @util.cached_property def age(self): """ :returns: The age of the user associated with this profile. """ if self.is_logged_in_user: # Retrieve the logged-in user's profile age return int(self._user_age_xpb.get_text_(self.profile_tree).strip()) else: # Retrieve a non logged-in user's profile age return int(self._age_xpb.get_text_(self.profile_tree)) _percentages_and_ratings_xpb = xpb.div.with_class('matchanalysis2015-graphs') @util.cached_property def match_percentage(self): """ :returns: The match percentage of the logged in user and the user associated with this object. """ return int(self._percentages_and_ratings_xpb. div.with_class('matchgraph--match'). div.with_class('matchgraph-graph'). canvas.select_attribute_('data-pct'). one_(self.profile_tree)) @util.cached_property def enemy_percentage(self): """ :returns: The enemy percentage of the logged in user and the user associated with this object. """ return int(self._percentages_and_ratings_xpb. div.with_class('matchgraph--enemy'). div.with_class('matchgraph-graph'). canvas.select_attribute_('data-pct'). one_(self.profile_tree)) _location_xpb = xpb.span.with_class('userinfo2015-basics-asl-location') _user_location_xpb = xpb.span(id='ajax_location') @util.cached_property def location(self): """ :returns: The location of the user associated with this profile. """ if self.is_logged_in_user: # Retrieve the logged-in user's profile location return self._user_location_xpb.get_text_(self.profile_tree) else: # Retrieve a non logged-in user's profile location return self._location_xpb.get_text_(self.profile_tree) @util.cached_property def gender(self): """The gender of the user associated with this profile.""" return xpb.span.with_class('ajax_gender').get_text_(self.profile_tree) @util.cached_property def orientation(self): """The sexual orientation of the user associated with this profile.""" return xpb.dd(id='ajax_orientation').\ get_text_(self.profile_tree).strip() @util.curry def message(self, message, thread_id=None): """Message the user associated with this profile. :param message: The message to send to this user. :param thread_id: The id of the thread to respond to, if any. """ return_value = helpers.Messager(self._session).send( self.username, message, self.authcode, thread_id ) self.refresh(reload=False) return return_value @util.cached_property def attractiveness(self): """ :returns: The average attractiveness rating given to this profile by the okcupid.com community. """ # This has to be here to avoid a circular import for now. from .attractiveness_finder import AttractivenessFinder return AttractivenessFinder(self._session)(self.username)
[docs] def toggle_like(self): """Toggle whether or not the logged in user likes this profile.""" return self.rate(1 if self.liked else 5)
[docs] def like(self): """Like this profile.""" return self.rate(5)
[docs] def unlike(self): """Unlike this profile.""" return self.rate(1)
[docs] def rate(self, rating): """Rate this profile as the user that was logged in with the session that this object was instantiated with. :param rating: The rating to give this user. """ parameters = { 'voterid': self._current_user_id, 'target_userid': self.id, 'type': 'vote', 'cf': 'profile2', 'target_objectid': 0, 'vote_type': 'personality', 'score': rating, } response = self._session.okc_post('vote_handler', data=parameters) response_json = response.json() log_function = log.info if response_json.get('status', False) \ else log.error log_function(simplejson.dumps({'rate_response': response_json, 'sent_parameters': parameters, 'headers': dict(self._session.headers)})) self.refresh(reload=False)
[docs] def find_question(self, question_id, question_fetchable=None): """ :param question_id: The id of the question to search for :param question_fetchable: The question fetchable to iterate through if none is provided `self.questions` will be used. """ question_fetchable = question_fetchable or self.questions for question in question_fetchable: if int(question.id) == int(question_id): return question
[docs] def question_fetchable(self, **kwargs): """ :returns: A :class:`~okcupyd.util.fetchable.Fetchable` instance that contains objects representing the answers that the user associated with this profile has given to okcupid.com match questions. """ return util.Fetchable(QuestionFetcher( self._session, self.username, is_user=self.is_logged_in_user, **kwargs ))
[docs] def authcode_get(self, path, **kwargs): """Perform an HTTP GET to okcupid.com using this profiles session where the authcode is automatically added as a query parameter. """ kwargs.setdefault('params', {})['authcode'] = self.authcode return self._session.okc_get(path, **kwargs)
[docs] def authcode_post(self, path, **kwargs): """Perform an HTTP POST to okcupid.com using this profiles session where the authcode is automatically added as a form item. """ kwargs.setdefault('data', {})['authcode'] = self.authcode return self._session.okc_post(path, **kwargs)
def __eq__(self, other): self.username.lower() == other.username.lower() def __repr__(self): return u'{0}("{1}")'.format(type(self).__name__, self.username)