Source code for okcupyd.details

# -*- coding: utf-8 -*-
import inspect
import logging
import re

from six import string_types

from . import helpers
from . import magicnumbers
from . import util
from .magicnumbers import maps
from .xpath import xpb


log = logging.getLogger(__name__)


[docs]class Detail(object): """Represent a detail belonging to an okcupid.com profile.""" NO_DEFAULT = object() @classmethod
[docs] def comma_separated_presenter(cls, text): return text.strip().split(', ')
@classmethod
[docs] def mapping_multi_updater(cls, mapping): def updater(id_name, value): if value is None: value = () return {id_name: [mapping[item.lower()] for item in value]} return updater
@classmethod
[docs] def auto_indexed_updater(cls, *options): return cls.mapping_updater({ option: index for index, option in enumerate(options, 1) }, default=0)
@classmethod
[docs] def mapping_updater(cls, mapping, id_name=None): return cls(id_name=id_name, updater=magicnumbers.MappingUpdater(mapping))
@staticmethod
[docs] def default_updater(id_name, value): return {id_name: value}
def __init__(self, id_name=None, presenter=None, updater=None): self.id_name = id_name self.presenter = presenter or (lambda x: None if u'\u2014' in x else helpers.replace_chars(x.strip())) self.updater = updater or self.default_updater @property def id_name(self): return self._id_name _doc_format = 'The {0} detail of an okcupid.com user\'s profile.' @id_name.setter def id_name(self, value): self._id_name = value self.__doc__ = self._doc_format.format(self.id_name)
[docs] def update(self, value): if isinstance(value, string_types): value = value.lower() return self.updater(self.id_name, value)
def __get__(self, details, klass): if details is None: return self return self.presenter(details.id_to_display_name_value.get(self.id_name, u'\u2014')) def __set__(self, details, value): details.update(self.update(value))
[docs]class DeclarativeDetail(object): updater = None presenter = None
[docs]class Details(object): """Represent the details belonging to an okcupid.com profile.""" @classmethod
[docs] def name_detail_pairs(cls): is_detail = lambda x: isinstance(x, Detail) return inspect.getmembers(cls, is_detail)
def __init__(self, profile): self.profile = profile _profile_details_xpb = xpb.div(id='profile_details').dl
[docs] def refresh(self): util.cached_property.bust_caches(self)
@util.cached_property def id_to_display_name_value(self): output = {} for element in self._profile_details_xpb.apply_( self.profile.profile_tree ): value_element = xpb.dd.one_(element) if not 'id' in value_element.attrib: continue id_name = value_element.attrib['id'].replace('ajax_', '') value = value_element.text_content() output[id_name] = value return output @property def as_dict(self): return {name: getattr(self, name) for name, _ in self.name_detail_pairs()}
[docs] def convert_and_update(self, data): klass = type(self) server_bound = {} for key, value in data.items(): detail = getattr(klass, key) server_bound.update(detail.update(value)) return self.update(server_bound)
[docs] def update(self, data): log.debug(data) response = self.profile.authcode_post('profileedit2', data=data) self.profile.refresh() self.refresh() return response
bodytype = Detail.mapping_updater(maps.bodytype) orientation = Detail.mapping_updater(maps.orientation) smokes = Detail.mapping_updater(maps.smokes, id_name='smoking') drugs = Detail.mapping_updater(maps.drugs) drinks = Detail.mapping_updater(maps.drinks, id_name='drinking') job = Detail.mapping_updater(maps.job) status = Detail.mapping_updater(maps.status) monogamy = Detail(id_name='monogamous', updater=lambda id_name, value: { 'monogamous': maps.monogamy[value], 'monogamyflex': maps.strictness[value] }) children = Detail(updater=lambda id_name, value: { 'children': maps.has_kids[value], 'children2': maps.wants_kids[value] }) education = Detail(updater=lambda id_name, value: { 'educationstatus': maps.education_status[value], 'educationlevel': maps.education_level[value] }) pets = Detail(updater=lambda id_name, value: { 'cats': maps.cats[value], 'dogs': maps.dogs[value] }) diet = Detail(updater=lambda id_name, value: { 'diet': maps.diet[value], 'dietserious': maps.diet_strictness[value] }) religion = Detail(updater=lambda id_name, value: { 'religion': maps.religion[value], 'religionserious': maps.seriousness[value] }) sign = Detail(updater=lambda id_name, value: { 'sign': maps.sign[value], 'sign_status': maps.importance[value] }) height = Detail(updater=lambda id_name, value: { 'centimeters': int(round(magicnumbers.parse_height_string(value))) })
[docs] class ethnicities(DeclarativeDetail): @staticmethod def presenter(text): return [ethnicity for ethnicity in Detail.comma_separated_presenter(text) if any(char.isalpha() for char in ethnicity)] @staticmethod def updater(id_name, value): if value is None: value = () ethnicities = [maps.ethnicities[item.lower()] for item in value] if len(ethnicities) < 1: ethnicities = 10 return {id_name: ethnicities}
[docs] class income(DeclarativeDetail): levels = list(enumerate( (20000, 30000, 40000, 50000, 60000, 70000, 80000, 100000, 150000, 250000, 500000, 1000000) )) comma_sep_number = '([0-9]{1,3}(?:,[0-9]{3})*)' range_matcher = re.compile(u'\$?{0}-\$?{0}'.format(comma_sep_number), flags=re.UNICODE) lt_matcher = re.compile("less than \$?{0}".format(comma_sep_number), flags=re.UNICODE) gt_matcher = re.compile("more than \$?{0}".format(comma_sep_number), flags=re.UNICODE) @classmethod def updater(cls, id_name, value): if value is None: return {'income': 0} if isinstance(value, string_types): for matcher, sign in ((cls.range_matcher, 1), (cls.lt_matcher, -1), (cls.gt_matcher, 1)): match = matcher.match(value) if match: matched_income = int(match.group(1).replace(',', '')) value = matched_income + 100 * sign break for index, level in cls.levels: if value < level: break else: index += 1 update = index + 1 return {'income': update}
[docs] class languages(DeclarativeDetail): language_matcher = re.compile('(.*?) \((.*?)\)') language_to_number = magicnumbers.language_map_2 level = util.IndexedREMap('fluently', 'okay', 'poorly') @classmethod def presenter(cls, value): language_strings = value.split(',') languages = [] for language_string in language_strings: match = cls.language_matcher.match(language_string.strip()) if match: languages.append((match.group(1).lower(), match.group(2).lower())) else: languages.append((language_string.strip(), None)) return languages @classmethod def updater(cls, id_name, languages): data = {} number = 0 for number, (language, level) in enumerate(languages, 1): language_number = cls.language_to_number[language.lower()] level = level or '' level_number = cls.level[level.lower()] data['cont_lang_{0}'.format(number)] = language_number data['language{0}status'.format(number)] = level_number number += 1 for i in range(number, 6): data['cont_lang_{0}'.format(i)] = '' data['language{0}status'.format(i)] = '' return data
for id_name, detail in Details.name_detail_pairs(): if detail.id_name is None: detail.id_name = id_name is_declarative_detail = lambda x: (isinstance(x, type) and issubclass(x, DeclarativeDetail)) for id_name, declarative_detail in inspect.getmembers( Details, is_declarative_detail ): detail = Detail(presenter=declarative_detail.presenter, updater=declarative_detail.updater, id_name=id_name) setattr(Details, id_name, detail)