# -*- 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)