Permalink
Browse files

changed directory structure to fix dumb mistake

  • Loading branch information...
1 parent 878d949 commit 00cdf4d6037a1205b4402caba0a28d2735dca9dd Aaron committed May 17, 2010
View
385 liclient/__init__.py
@@ -0,0 +1,385 @@
+#! usr/bin/env python
+
+import httplib, re, datetime, time
+import urlparse
+import urllib
+import oauth2 as oauth
+from parsers.lixml import LinkedInXMLParser
+from lxml import etree
+from lxml.builder import ElementMaker
+
+class LinkedInAPI(object):
+ def __init__(self, ck, cs):
+ self.consumer_key = ck
+ self.consumer_secret = cs
+
+ self.api_profile_url = 'http://api.linkedin.com/v1/people/~'
+ self.api_profile_connections_url = 'http://api.linkedin.com/v1/people/~/connections'
+ self.api_network_update_url = 'http://api.linkedin.com/v1/people/~/network'
+ self.api_comment_feed_url = 'http://api.linkedin.com/v1/people/~/network/updates/' + \
+ 'key={NETWORK UPDATE KEY}/update-comments'
+ self.api_update_status_url = 'http://api.linkedin.com/v1/people/~/current-status'
+ self.api_mailbox_url = 'http://api.linkedin.com/v1/people/~/mailbox'
+
+ self.base_url = 'https://api.linkedin.com'
+ self.li_url = 'http://www.linkedin.com'
+
+ self.request_token_path = '/uas/oauth/requestToken'
+ self.access_token_path = '/uas/oauth/accessToken'
+ self.authorize_path = '/uas/oauth/authorize'
+
+ self.consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
+
+ self.valid_network_update_codes = ['ANSW', 'APPS', 'CONN', 'JOBS',
+ 'JGRP', 'PICT', 'RECU', 'PRFU',
+ 'QSTN', 'STAT']
+
+ def get_request_token(self):
+ """
+ Get a request token based on the consumer key and secret to supply the
+ user with the authorization URL they can use to give the application
+ access to their LinkedIn accounts
+ """
+ client = oauth.Client(self.consumer)
+ request_token_url = self.base_url + self.request_token_path
+
+ resp, content = client.request(request_token_url, 'POST')
+ request_token = dict(urlparse.parse_qsl(content))
+ return request_token
+
+ def get_access_token(self, request_token, verifier):
+ """
+ Get an access token based on the generated request_token and the
+ oauth verifier supplied in the return URL when a user authorizes their
+ application
+ """
+ token = oauth.Token(request_token['oauth_token'],
+ request_token['oauth_token_secret'])
+ token.set_verifier(verifier)
+ client = oauth.Client(self.consumer, token)
+ access_token_url = self.base_url + self.access_token_path
+
+ resp, content = client.request(access_token_url, 'POST')
+ access_token = dict(urlparse.parse_qsl(content))
+ return access_token
+
+ def get_user_profile(self, access_token, selectors=None, **kwargs):
+ """
+ Get a user profile. If keyword argument "id" is not supplied, this
+ returns the current user's profile, else it will return the profile of
+ the user whose id is specificed. The "selectors" keyword argument takes
+ a list of LinkedIn compatible field selectors.
+ """
+
+ assert type(selectors) == type([]), '"Keyword argument "selectors" must be of type "list"'
+ user_token, url = self.prepare_request(access_token, self.api_profile_url, kwargs)
+ client = oauth.Client(self.consumer, user_token)
+
+ if not selectors:
+ resp, content = client.request(self.api_profile_url, 'GET')
+ else:
+ url = self.prepare_field_selectors(selectors, url)
+ resp, content = client.request(url, 'GET')
+
+ content = self.clean_dates(content)
+ return LinkedInXMLParser(content).results
+
+ def get_user_connections(self, access_token, selectors=None, **kwargs):
+ """
+ Get the connections of the current user. Valid keyword arguments are
+ "count" and "start" for the number of profiles you wish returned. Types
+ are automatically converted from integer to string for URL formatting
+ if necessary.
+ """
+
+ user_token, url = self.prepare_request(access_token, self.api_profile_connections_url, kwargs)
+ client = oauth.Client(self.consumer, user_token)
+ if not selectors:
+ resp, content = client.request(url, 'GET')
+ else:
+ url = self.prepare_field_selectors(selectors, url)
+ resp, content = client.request(url, 'GET')
+ content = self.clean_dates(content)
+ return LinkedInXMLParser(content).results
+
+ def get_network_updates(self, access_token, **kwargs):
+ """Get network updates for the current user. Valid keyword arguments are
+ "count", "start", "type", "before", and "after". "Count" and "start" are for the number
+ of updates to be returned. "Type" specifies what type of update you are querying.
+ "Before" and "after" set the time interval for the query. Valid argument types are
+ an integer representing UTC with millisecond precision or a Python datetime object.
+ """
+ if 'type' in kwargs.keys():
+ assert type(kwargs['type']) == type(list()), 'Keyword argument "type" must be of type "list"'
+ [self.check_network_code(c) for c in kwargs['type']]
+
+ if 'before' in kwargs.keys():
+ kwargs['before'] = self.dt_obj_to_string(kwargs['before']) if kwargs['before'] else None
+ if 'after' in kwargs.keys():
+ kwargs['after'] = self.dt_obj_to_string(kwargs['after']) if kwargs['after'] else None
+
+ user_token, url = self.prepare_request(access_token, self.api_network_update_url, kwargs)
+ client = oauth.Client(self.consumer, user_token)
+ resp, content = client.request(url, 'GET')
+ content = self.clean_dates(content)
+ return LinkedInXMLParser(content).results
+
+ def get_comment_feed(self, access_token, network_key):
+ """
+ Get a comment feed for a particular network update. Requires the update key
+ for the network update as returned by the API.
+ """
+ url = re.sub(r'\{NETWORK UPDATE KEY\}', network_key, self.api_comment_feed_url)
+ user_token, url = self.prepare_request(access_token, url)
+ client = oauth.Client(self.consumer, user_token)
+ resp, content = client.request(url, 'GET')
+ content = self.clean_dates(content)
+ return LinkedInXMLParser(content).results
+
+ def submit_comment(self, access_token, network_key, bd):
+ """
+ Submit a comment to a network update. Requires the update key for the network
+ update that you will be commenting on. The comment body is the last positional
+ argument. NOTE: The XML will be applied to the comment for you.
+ """
+ bd_pre_wrapper = '<?xml version="1.0" encoding="UTF-8"?><update-comment><comment>'
+ bd_post_wrapper = '</comment></update-comment>'
+ xml_request = bd_pre_wrapper + bd + bd_post_wrapper
+ url = re.sub(r'\{NETWORK UPDATE KEY\}', network_key, self.api_comment_feed_url)
+ user_token, url = self.prepare_request(access_token, url)
+ client = oauth.Client(self.consumer, user_token)
+ resp, content = client.request(url, method='POST', body=xml_request, headers={'Content-Type': 'application/xml'})
+ return content
+
+ def set_status_update(self, access_token, bd):
+ """
+ Set the status for the current user. The status update body is the last
+ positional argument. NOTE: The XML will be applied to the status update
+ for you.
+ """
+ bd_pre_wrapper = '<?xml version="1.0" encoding="UTF-8"?><current-status>'
+ bd_post_wrapper = '</current-status>'
+ xml_request = bd_pre_wrapper + bd + bd_post_wrapper
+ user_token, url = self.prepare_request(access_token, self.api_update_status_url)
+ client = oauth.Client(self.consumer, user_token)
+ resp, content = client.request(url, method='PUT', body=xml_request)
+ return content
+
+ def search(self, access_token, data):
+ srch = LinkedInSearchAPI(data, access_token)
+ client = oauth.Client(self.consumer, srch.user_token)
+ rest, content = client.request(srch.generated_url, method='GET')
+ return LinkedInXMLParser(content).results
+
+ def send_message(self, access_token, recipients, subject, body):
+ assert type(recipients) == type(list()), '"Recipients argument" (2st position) must be of type "list"'
+ mxml = self.message_factory(recipients, subject, body)
+ user_token, url = self.prepare_request(access_token, self.api_mailbox_url)
+ client = oauth.Client(self.consumer, user_token)
+ resp, content = client.request(url, method='POST', body=mxml, headers={'Content-Type': 'application/xml'})
+ return content
+
+ def prepare_request(self, access_token, url, kws=[]):
+ user_token = oauth.Token(access_token['oauth_token'],
+ access_token['oauth_token_secret'])
+ prep_url = url
+ if kws and 'id' in kws.keys():
+ prep_url = self.append_id_args(kws['id'], prep_url)
+ del kws['id']
+ for k in kws:
+ if kws[k]:
+ if '?' not in prep_url:
+ prep_url = self.append_initial_arg(k, kws[k], prep_url)
+ else:
+ prep_url = self.append_sequential_arg(k, kws[k], prep_url)
+ prep_url = re.sub('&&', '&', prep_url)
+ print prep_url
+ return user_token, prep_url
+
+ def append_id_args(self, ids, prep_url):
+ assert type(ids) == type([]), 'Keyword argument "id" must be a list'
+ if len(ids) > 1:
+ prep_url = re.sub('/~', '::(', prep_url) #sub out the ~ if a user wants someone else's profile
+ for i in ids:
+ prep_url += 'id='+i+','
+ prep_url = re.sub(',$', ')', prep_url)
+ else:
+ prep_url = re.sub('~', 'id='+ids[0], prep_url)
+ return prep_url
+
+ def append_initial_arg(self, key, args, prep_url):
+ assert '?' not in prep_url, 'Initial argument has already been applied to %s' % prep_url
+ if type(args) == type([]):
+ prep_url += '?' + key + '=' + str(args[0])
+ if len(args) > 1:
+ prep_url += ''.join(['&' + key + '=' + str(arg) for arg in args[1:]])
+ else:
+ prep_url += '?' + key + '=' + str(args)
+ return prep_url
+
+ def append_sequential_arg(self, key, args, prep_url):
+ if type(args) == type([]):
+ prep_url += '&' + ''.join(['&'+key+'='+str(arg) for arg in args])
+ else:
+ prep_url += '&' + key + '=' + str(args)
+ return prep_url
+
+ def prepare_field_selectors(self, selectors, url):
+ prep_url = url
+ selector_string = ':('
+ for s in selectors:
+ selector_string += s + ','
+ selector_string = selector_string.strip(',')
+ selector_string += ')'
+ prep_url += selector_string
+ print prep_url
+ return prep_url
+
+ def check_network_code(self, code):
+ if code not in self.valid_network_update_codes:
+ raise ValueError('Code %s not a valid update code' % code)
+
+ def clean_dates(self, content):
+ data = etree.fromstring(content)
+ for d in data.iter(tag=etree.Element):
+ try:
+ trial = int(d.text)
+ if len(d.text) > 8:
+ dt = datetime.datetime.fromtimestamp(float(trial)/1000)
+ d.text = dt.strftime('%m/%d/%Y %I:%M:%S')
+ except:
+ continue
+ return etree.tostring(data)
+
+ def dt_obj_to_string(self, dtobj):
+ if type(dtobj) == type(int()) or type(dtobj) == type(str()) or type(dtobj) == type(long()):
+ return dtobj
+ elif hasattr(dtobj, 'timetuple'):
+ return time.mktime(int(dtobj.timetuple())*1000)
+ else:
+ raise TypeError('Inappropriate argument type - use either a datetime object, \
+ string, or integer for timestamps')
+
+ def message_factory(self, recipients, subject, body):
+ rec_path = '/people/'
+
+ E = ElementMaker()
+ MAILBOX_ITEM = E.mailbox_item
+ RECIPIENTS = E.recipients
+ RECIPIENT = E.recipient
+ PERSON = E.person
+ SUBJECT = E.subject
+ BODY = E.body
+
+ recs = [RECIPIENT(PERSON(path=rec_path+r)) for r in recipients]
+
+ mxml = MAILBOX_ITEM(
+ RECIPIENTS(
+ *recs
+ ),
+ SUBJECT(subject),
+ BODY(body)
+ )
+ return re.sub('mailbox_item', 'mailbox-item', etree.tostring(mxml))
+
+class LinkedInSearchAPI(LinkedInAPI):
+ def __init__(self, params, access_token):
+ self.api_search_url = 'http://api.linkedin.com/v1/people/'
+ self.routing = {
+ 'keywords': self.keywords,
+ 'name': self.name,
+ 'current_company': self.current_company,
+ 'current_title': self.current_title,
+ 'location_type': self.location_type,
+ 'network': self.network,
+ 'sort_criteria': self.sort_criteria
+ }
+ self.user_token, self.generated_url = self.do_process(access_token, params)
+
+ def do_process(self, access_token, params):
+ assert type(params) == type(dict()), 'The passed parameters to the Search API must be a dictionary.'
+ user_token = oauth.Token(access_token['oauth_token'],
+ access_token['oauth_token_secret'])
+ url = self.api_search_url
+ for p in params:
+ try:
+ url = self.routing[p](url, params[p])
+ params[p] = None
+ except KeyError:
+ continue
+ remaining_params = {}
+ for p in params:
+ if params[p]:
+ remaining_params[p] = params[p]
+ url = self.process_remaining_params(url, remaining_params)
+ return user_token, url
+
+ def process_remaining_params(self, url, ps):
+ prep_url = url
+ for p in ps:
+ try:
+ prep_url = self.append_initial_arg(p, ps[p], prep_url)
+ except AssertionError:
+ prep_url = self.append_sequential_arg(p, ps[p], prep_url)
+ return prep_url
+
+ def keywords(self, url, ps):
+ return self.list_argument(url, ps, 'keywords')
+
+ def name(self, url, ps):
+ return self.list_argument(url, ps, 'name')
+
+ def current_company(self, url, ps):
+ return self.true_false_argument(url, ps, 'current-company')
+
+ def current_title(self, url, ps):
+ return self.true_false_argument(url, ps, 'current-title')
+
+ def location_type(self, url, ps):
+ prep_url = url
+ assert ps in ['I', 'Y'], 'Valid parameter types for search-location-type are "I" and "Y"'
+ try:
+ prep_url = self.append_initial_arg('search-location-type', ps, prep_url)
+ except AssertionError:
+ prep_url = self.append_sequential_arg('search-location-type', ps, prep_url)
+ return prep_url
+
+ def network(self, url, ps):
+ prep_url = url
+ assert ps in ['in', 'out'], 'Valid parameter types for network are "in" and "out"'
+ try:
+ prep_url = self.append_initial_arg('network', ps, prep_url)
+ except AssertionError:
+ prep_url = self.append_sequential_arg('network', ps, prep_url)
+ return prep_url
+
+ def sort_criteria(self):
+ prep_url = url
+ assert ps in ['recommenders', 'distance', 'relevance'], 'Valid parameter types for sort-criteria \
+ are "recommenders", "distance", and "relevance"'
+ try:
+ prep_url = self.append_initial_arg('sort-criteria', ps, prep_url)
+ except AssertionError:
+ prep_url = self.append_sequential_arg('sort-criteria', ps, prep_url)
+ return prep_url
+
+ def true_false_argument(self, url, ps, arg):
+ prep_url = url
+ if ps:
+ ps = 'true'
+ else:
+ ps = 'false'
+ try:
+ prep_url = self.append_initial_arg(arg, ps, prep_url)
+ except AssertionError:
+ prep_url = self.append_sequential_arg(arg, ps, prep_url)
+ return prep_url
+
+ def list_argument(self, url, ps, arg):
+ prep_url = url
+ li = '+'.join(ps)
+ try:
+ prep_url = self.append_initial_arg(arg, li, prep_url)
+ except AssertionError:
+ prep_url = self.append_sequential_arg(arg, li, prep_url)
+ return prep_url
View
BIN liclient/__init__.pyc
Binary file not shown.
View
0 liclient/analysis/__init__.py
No changes.
View
45 liclient/analysis/nlp.py
@@ -0,0 +1,45 @@
+#! usr/bin/env python
+import re, nltk
+
+class TextualAnalyzer(object):
+ def __init__(self, txt, source):
+ self.sources = {}
+ text = nltk.text.Text(txt)
+ self.sources[source] = {}
+ self.cfds = {}
+ self.sources[source]['text'] = text
+ self.sources[source]['collocations'] = text.collocations().split(';')
+ self.sources[source]['freq_dist'] = text.vocab()
+
+ def register(self, txt, source):
+ if source not in self.sources.keys():
+ text = nltk.text.Text(txt)
+ self.sources[source] = {}
+ self.sources[source]['text'] = text.tokens
+ self.sources[source]['collocations'] = text.collocations().split(';')
+ self.sources[source]['freq_dist'] = text.vocab()
+ else:
+ raise KeyError('Source already found in internal dictionary. Please use a different source name.')
+
+ def generate_cfd(self, srca, srcb):
+ cdna = [(w, srca) for w in self.sources[srca]['text']]
+ cdnb = [(w, srcb) for w in self.sources[srcb]['text']]
+ cfdp = [cdnb + cdna]
+ self.cfds[srca+ ', ' + srcb] = nltk.ConditionalFreqDist(cfdp)
+
+ def tag(self, source, tagger=None):
+ if not tagger:
+ self.sources[source]['tagged'] = nltk.pos_tag(self.sources[source]['text'])
+ else:
+ self.sources[source]['tagged'] = tagger.tag(self.sources[source]['text'])
+
+ def chunk(self, source, chunker=None):
+ if not self.sources[source]['tagged']:
+ self.tag(source)
+ grammar = r"""
+ NP: {<DT|PP>?<JJ.*>*<NN.*>}
+ {<NNP>+}
+ VP: {<JJ.*>?<RB>?<VB+><NN.*>*}
+ """
+ cp = nltk.RegexpParser(grammar)
+ self.sources[source]['chunked'] = cp.parse(self.sources[source]['tagged'])
View
702 liclient/oauth/__init__.py
@@ -0,0 +1,702 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+import httplib2
+
+try:
+ from urlparse import parse_qs, parse_qsl
+except ImportError:
+ from cgi import parse_qs, parse_qsl
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class Error(RuntimeError):
+ """Generic exception class."""
+
+ def __init__(self, message='OAuth error occured.'):
+ self._message = message
+
+ @property
+ def message(self):
+ """A hack to get around the deprecation errors in 2.6."""
+ return self._message
+
+ def __str__(self):
+ return self._message
+
+class MissingSignature(Error):
+ pass
+
+def build_authenticate_header(realm=''):
+ """Optional WWW-Authenticate header (401 error)"""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+
+def escape(s):
+ """Escape a URL including any /."""
+ return urllib.quote(s, safe='~')
+
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC)."""
+ return int(time.time())
+
+
+def generate_nonce(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+def generate_verifier(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class Consumer(object):
+ """A consumer of OAuth-protected services.
+The OAuth consumer is a "third-party" service that wants to access
+protected resources from an OAuth service provider on behalf of an end
+user. It's kind of the OAuth client.
+Usually a consumer must be registered with the service provider by the
+developer of the consumer software. As part of that process, the service
+provider gives the consumer a *key* and a *secret* with which the consumer
+software can identify itself to the service. The consumer will include its
+key in each request to identify itself, but will use its secret only when
+signing requests, to prove that the request is from that particular
+registered consumer.
+Once registered, the consumer can then use its consumer credentials to ask
+the service provider for a request token, kicking off the OAuth
+authorization process.
+"""
+
+ key = None
+ secret = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+ if self.key is None or self.secret is None:
+ raise ValueError("Key and secret must be set.")
+
+ def __str__(self):
+ data = {
+ 'oauth_consumer_key': self.key,
+ 'oauth_consumer_secret': self.secret
+ }
+
+ return urllib.urlencode(data)
+
+
+class Token(object):
+ """An OAuth credential used to request authorization or a protected
+resource.
+Tokens in OAuth comprise a *key* and a *secret*. The key is included in
+requests to identify the token being used, but the secret is used only in
+the signature, to prove that the requester is who the server gave the
+token to.
+When first negotiating the authorization, the consumer asks for a *request
+token* that the live user authorizes with the service provider. The
+consumer then exchanges the request token for an *access token* that can
+be used to access protected resources.
+"""
+
+ key = None
+ secret = None
+ callback = None
+ callback_confirmed = None
+ verifier = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+ if self.key is None or self.secret is None:
+ raise ValueError("Key and secret must be set.")
+
+ def set_callback(self, callback):
+ self.callback = callback
+ self.callback_confirmed = 'true'
+
+ def set_verifier(self, verifier=None):
+ if verifier is not None:
+ self.verifier = verifier
+ else:
+ self.verifier = generate_verifier()
+
+ def get_callback_url(self):
+ if self.callback and self.verifier:
+ # Append the oauth_verifier.
+ parts = urlparse.urlparse(self.callback)
+ scheme, netloc, path, params, query, fragment = parts[:6]
+ if query:
+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
+ else:
+ query = 'oauth_verifier=%s' % self.verifier
+ return urlparse.urlunparse((scheme, netloc, path, params,
+ query, fragment))
+ return self.callback
+
+ def to_string(self):
+ """Returns this token as a plain string, suitable for storage.
+The resulting string includes the token's secret, so you should never
+send or store this string where a third party can read it.
+"""
+
+ data = {
+ 'oauth_token': self.key,
+ 'oauth_token_secret': self.secret,
+ }
+
+ if self.callback_confirmed is not None:
+ data['oauth_callback_confirmed'] = self.callback_confirmed
+ return urllib.urlencode(data)
+
+ @staticmethod
+ def from_string(s):
+ """Deserializes a token from a string like one returned by
+`to_string()`."""
+
+ if not len(s):
+ raise ValueError("Invalid parameter string.")
+
+ params = parse_qs(s, keep_blank_values=False)
+ if not len(params):
+ raise ValueError("Invalid parameter string.")
+
+ try:
+ key = params['oauth_token'][0]
+ except Exception:
+ raise ValueError("'oauth_token' not found in OAuth request.")
+
+ try:
+ secret = params['oauth_token_secret'][0]
+ except Exception:
+ raise ValueError("'oauth_token_secret' not found in "
+ "OAuth request.")
+
+ token = Token(key, secret)
+ try:
+ token.callback_confirmed = params['oauth_callback_confirmed'][0]
+ except KeyError:
+ pass # 1.0, no callback confirmed.
+ return token
+
+ def __str__(self):
+ return self.to_string()
+
+
+def setter(attr):
+ name = attr.__name__
+
+ def getter(self):
+ try:
+ return self.__dict__[name]
+ except KeyError:
+ raise AttributeError(name)
+
+ def deleter(self):
+ del self.__dict__[name]
+
+ return property(getter, attr, deleter)
+
+
+class Request(dict):
+
+ """The parameters and information for an HTTP request, suitable for
+authorizing with OAuth credentials.
+When a consumer wants to access a service's protected resources, it does
+so using a signed HTTP request identifying itself (the consumer) with its
+key, and providing an access token authorized by the end user to access
+those resources.
+"""
+
+ version = VERSION
+
+ def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
+ self.method = method
+ self.url = url
+ if parameters is not None:
+ self.update(parameters)
+
+ @setter
+ def url(self, value):
+ self.__dict__['url'] = value
+ if value is not None:
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
+
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ if scheme not in ('http', 'https'):
+ raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
+
+ # Normalized URL excludes params, query, and fragment.
+ self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
+ else:
+ self.normalized_url = None
+ self.__dict__['url'] = None
+
+ @setter
+ def method(self, value):
+ self.__dict__['method'] = value.upper()
+
+ def _get_timestamp_nonce(self):
+ return self['oauth_timestamp'], self['oauth_nonce']
+
+ def get_nonoauth_parameters(self):
+ """Get any non-OAuth parameters."""
+ return dict([(k, v) for k, v in self.iteritems()
+ if not k.startswith('oauth_')])
+
+ def to_header(self, realm=''):
+ """Serialize as a header for an HTTPAuth request."""
+ oauth_params = ((k, v) for k, v in self.items()
+ if k.startswith('oauth_'))
+ stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
+ header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
+ params_header = ', '.join(header_params)
+
+ auth_header = 'OAuth realm="%s"' % realm
+ if params_header:
+ auth_header = "%s, %s" % (auth_header, params_header)
+
+ return {'Authorization': auth_header}
+
+ def to_postdata(self):
+ """Serialize as post data for a POST request."""
+ # tell urlencode to deal with sequence values and map them correctly
+ # to resulting querystring. for example self["k"] = ["v1", "v2"] will
+ # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
+ return urllib.urlencode(self, True)
+
+ def to_url(self):
+ """Serialize as a URL for a GET request."""
+ base_url = urlparse.urlparse(self.url)
+ query = parse_qs(base_url.query)
+ for k, v in self.items():
+ query.setdefault(k, []).append(v)
+ url = (base_url.scheme, base_url.netloc, base_url.path, base_url.params,
+ urllib.urlencode(query, True), base_url.fragment)
+ return urlparse.urlunparse(url)
+
+ def get_parameter(self, parameter):
+ ret = self.get(parameter)
+ if ret is None:
+ raise Error('Parameter not found: %s' % parameter)
+
+ return ret
+
+ def get_normalized_parameters(self):
+ """Return a string that contains the parameters that must be signed."""
+ items = []
+ for key, value in self.iteritems():
+ if key == 'oauth_signature':
+ continue
+ # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
+ # so we unpack sequence values into multiple items for sorting.
+ if hasattr(value, '__iter__'):
+ items.extend((key, item) for item in value)
+ else:
+ items.append((key, value))
+
+ # Include any query string parameters from the provided URL
+ query = urlparse.urlparse(self.url)[4]
+ items.extend(self._split_url_string(query).items())
+
+ encoded_str = urllib.urlencode(sorted(items))
+ # Encode signature parameters per Oauth Core 1.0 protocol
+ # spec draft 7, section 3.6
+ # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
+ # Spaces must be encoded with "%20" instead of "+"
+ return encoded_str.replace('+', '%20')
+
+ def sign_request(self, signature_method, consumer, token):
+ """Set the signature parameter to the result of sign."""
+
+ if 'oauth_consumer_key' not in self:
+ self['oauth_consumer_key'] = consumer.key
+
+ if token and 'oauth_token' not in self:
+ self['oauth_token'] = token.key
+
+ self['oauth_signature_method'] = signature_method.name
+ self['oauth_signature'] = signature_method.sign(self, consumer, token)
+
+ @classmethod
+ def make_timestamp(cls):
+ """Get seconds since epoch (UTC)."""
+ return str(int(time.time()))
+
+ @classmethod
+ def make_nonce(cls):
+ """Generate pseudorandom number."""
+ return str(random.randint(0, 100000000))
+
+ @classmethod
+ def from_request(cls, http_method, http_url, headers=None, parameters=None,
+ query_string=None):
+ """Combines multiple parameter sources."""
+ if parameters is None:
+ parameters = {}
+
+ # Headers
+ if headers and 'Authorization' in headers:
+ auth_header = headers['Authorization']
+ # Check that the authorization header is OAuth.
+ if auth_header[:6] == 'OAuth ':
+ auth_header = auth_header[6:]
+ try:
+ # Get the parameters from the header.
+ header_params = cls._split_header(auth_header)
+ parameters.update(header_params)
+ except:
+ raise Error('Unable to parse OAuth parameters from '
+ 'Authorization header.')
+
+ # GET or POST query string.
+ if query_string:
+ query_params = cls._split_url_string(query_string)
+ parameters.update(query_params)
+
+ # URL parameters.
+ param_str = urlparse.urlparse(http_url)[4] # query
+ url_params = cls._split_url_string(param_str)
+ parameters.update(url_params)
+
+ if parameters:
+ return cls(http_method, http_url, parameters)
+
+ return None
+
+ @classmethod
+ def from_consumer_and_token(cls, consumer, token=None,
+ http_method=HTTP_METHOD, http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ defaults = {
+ 'oauth_consumer_key': consumer.key,
+ 'oauth_timestamp': cls.make_timestamp(),
+ 'oauth_nonce': cls.make_nonce(),
+ 'oauth_version': cls.version,
+ }
+
+ defaults.update(parameters)
+ parameters = defaults
+
+ if token:
+ parameters['oauth_token'] = token.key
+ if token.verifier:
+ parameters['oauth_verifier'] = token.verifier
+
+ return Request(http_method, http_url, parameters)
+
+ @classmethod
+ def from_token_and_callback(cls, token, callback=None,
+ http_method=HTTP_METHOD, http_url=None, parameters=None):
+
+ if not parameters:
+ parameters = {}
+
+ parameters['oauth_token'] = token.key
+
+ if callback:
+ parameters['oauth_callback'] = callback
+
+ return cls(http_method, http_url, parameters)
+
+ @staticmethod
+ def _split_header(header):
+ """Turn Authorization: header into parameters."""
+ params = {}
+ parts = header.split(',')
+ for param in parts:
+ # Ignore realm parameter.
+ if param.find('realm') > -1:
+ continue
+ # Remove whitespace.
+ param = param.strip()
+ # Split key-value.
+ param_parts = param.split('=', 1)
+ # Remove quotes and unescape the value.
+ params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+ return params
+
+ @staticmethod
+ def _split_url_string(param_str):
+ """Turn URL string into parameters."""
+ parameters = parse_qs(param_str, keep_blank_values=False)
+ for k, v in parameters.iteritems():
+ parameters[k] = urllib.unquote(v[0])
+ return parameters
+
+
+class Server(object):
+ """A skeletal implementation of a service provider, providing protected
+resources to requests from authorized consumers.
+This class implements the logic to check requests for authorization. You
+can use it with your web server or web framework to protect certain
+resources with OAuth.
+"""
+
+ timestamp_threshold = 300 # In seconds, five minutes.
+ version = VERSION
+ signature_methods = None
+
+ def __init__(self, signature_methods=None):
+ self.signature_methods = signature_methods or {}
+
+ def add_signature_method(self, signature_method):
+ self.signature_methods[signature_method.name] = signature_method
+ return self.signature_methods
+
+ def verify_request(self, request, consumer, token):
+ """Verifies an api call and checks all the parameters."""
+
+ version = self._get_version(request)
+ self._check_signature(request, consumer, token)
+ parameters = request.get_nonoauth_parameters()
+ return parameters
+
+ def build_authenticate_header(self, realm=''):
+ """Optional support for the authenticate header."""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+ def _get_version(self, request):
+ """Verify the correct version request for this server."""
+ try:
+ version = request.get_parameter('oauth_version')
+ except:
+ version = VERSION
+
+ if version and version != self.version:
+ raise Error('OAuth version %s not supported.' % str(version))
+
+ return version
+
+ def _get_signature_method(self, request):
+ """Figure out the signature with some defaults."""
+ try:
+ signature_method = request.get_parameter('oauth_signature_method')
+ except:
+ signature_method = SIGNATURE_METHOD
+
+ try:
+ # Get the signature method object.
+ signature_method = self.signature_methods[signature_method]
+ except:
+ signature_method_names = ', '.join(self.signature_methods.keys())
+ raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
+
+ return signature_method
+
+ def _get_verifier(self, request):
+ return request.get_parameter('oauth_verifier')
+
+ def _check_signature(self, request, consumer, token):
+ timestamp, nonce = request._get_timestamp_nonce()
+ self._check_timestamp(timestamp)
+ signature_method = self._get_signature_method(request)
+
+ try:
+ signature = request.get_parameter('oauth_signature')
+ except:
+ raise MissingSignature('Missing oauth_signature.')
+
+ # Validate the signature.
+ valid = signature_method.check(request, consumer, token, signature)
+
+ if not valid:
+ key, base = signature_method.signing_base(request, consumer, token)
+
+ raise Error('Invalid signature. Expected signature base '
+ 'string: %s' % base)
+
+ built = signature_method.sign(request, consumer, token)
+
+ def _check_timestamp(self, timestamp):
+ """Verify that timestamp is recentish."""
+ timestamp = int(timestamp)
+ now = int(time.time())
+ lapsed = now - timestamp
+ if lapsed > self.timestamp_threshold:
+ raise Error('Expired timestamp: given %d and now %s has a '
+ 'greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
+
+
+class Client(httplib2.Http):
+ """OAuthClient is a worker to attempt to execute a request."""
+
+ def __init__(self, consumer, token=None, cache=None, timeout=None,
+ proxy_info=None):
+
+ if consumer is not None and not isinstance(consumer, Consumer):
+ raise ValueError("Invalid consumer.")
+
+ if token is not None and not isinstance(token, Token):
+ raise ValueError("Invalid token.")
+
+ self.consumer = consumer
+ self.token = token
+ self.method = SignatureMethod_HMAC_SHA1()
+
+ httplib2.Http.__init__(self, cache=cache, timeout=timeout,
+ proxy_info=proxy_info)
+
+ def set_signature_method(self, method):
+ if not isinstance(method, SignatureMethod):
+ raise ValueError("Invalid signature method.")
+
+ self.method = method
+
+ def request(self, uri, method="GET", body=None, headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
+ DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
+
+ if not isinstance(headers, dict):
+ headers = {}
+
+ is_multipart = method == 'POST' and headers.get('Content-Type', DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE
+
+ if body and method == "POST" and not is_multipart:
+ parameters = dict(parse_qsl(body))
+ else:
+ parameters = None
+
+ req = Request.from_consumer_and_token(self.consumer, token=self.token,
+ http_method=method, http_url=uri, parameters=parameters)
+
+ req.sign_request(self.method, self.consumer, self.token)
+
+
+ if method == "POST":
+ headers['Content-Type'] = headers.get('Content-Type', DEFAULT_CONTENT_TYPE)
+ if is_multipart:
+ headers.update(req.to_header())
+ else:
+ body = req.to_postdata()
+ elif method == "GET":
+ uri = req.to_url()
+ else:
+ headers.update(req.to_header())
+
+ return httplib2.Http.request(self, uri, method=method, body=body,
+ headers=headers, redirections=redirections,
+ connection_type=connection_type)
+
+
+class SignatureMethod(object):
+ """A way of signing requests.
+The OAuth protocol lets consumers and service providers pick a way to sign
+requests. This interface shows the methods expected by the other `oauth`
+modules for signing requests. Subclass it and implement its methods to
+provide a new way to sign requests.
+"""
+
+ def signing_base(self, request, consumer, token):
+ """Calculates the string that needs to be signed.
+
+This method returns a 2-tuple containing the starting key for the
+signing and the message to be signed. The latter may be used in error
+messages to help clients debug their software.
+
+"""
+ raise NotImplementedError
+
+ def sign(self, request, consumer, token):
+ """Returns the signature for the given request, based on the consumer
+and token also provided.
+
+You should use your implementation of `signing_base()` to build the
+message to sign. Otherwise it may be less useful for debugging.
+
+"""
+ raise NotImplementedError
+
+ def check(self, request, consumer, token, signature):
+ """Returns whether the given signature is the correct signature for
+the given consumer and token signing the given request."""
+ built = self.sign(request, consumer, token)
+ return built == signature
+
+
+class SignatureMethod_HMAC_SHA1(SignatureMethod):
+ name = 'HMAC-SHA1'
+
+ def signing_base(self, request, consumer, token):
+ sig = (
+ escape(request.method),
+ escape(request.normalized_url),
+ escape(request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % escape(consumer.secret)
+ if token:
+ key += escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def sign(self, request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.signing_base(request, consumer, token)
+
+ # HMAC object.
+ try:
+ from hashlib import sha1 as sha
+ except ImportError:
+ import sha # Deprecated
+
+ hashed = hmac.new(key, raw, sha)
+
+ # Calculate the digest base 64.
+ return binascii.b2a_base64(hashed.digest())[:-1]
+
+class SignatureMethod_PLAINTEXT(SignatureMethod):
+
+ name = 'PLAINTEXT'
+
+ def signing_base(self, request, consumer, token):
+ """Concatenates the consumer key and secret with the token's
+ secret."""
+ sig = '%s&' % escape(consumer.secret)
+ if token:
+ sig = sig + escape(token.secret)
+ return sig, sig
+
+ def sign(self, request, consumer, token):
+ key, raw = self.signing_base(request, consumer, token)
+ return raw
View
0 liclient/parsers/__init__.py
No changes.
View
BIN liclient/parsers/__init__.pyc
Binary file not shown.
View
9 liclient/parsers/helpers.py
@@ -0,0 +1,9 @@
+#! usr/bin/env python
+
+def create_json(objs):
+ assert type(objs) == type(dict()), 'Passed object must be a dict with keys "results" and "total"'
+ json_skeleton = {'total': objs['total'], 'results': [], 'success': True}
+ for obj in objs['results']:
+ assert hasattr(obj, 'jsonify'), 'Passed objects must be convertible to json with a jsonify() method'
+ json_skeleton['results'].append(obj.jsonify())
+ return json_skeleton
View
BIN liclient/parsers/helpers.pyc
Binary file not shown.
View
335 liclient/parsers/lixml.py
@@ -0,0 +1,335 @@
+from lxml import etree
+import mappers
+import re
+
+class LinkedInXMLParser(object):
+ def __init__(self, content):
+ self.routing = {
+ 'network': self.__parse_network_updates,
+ 'person': self.__parse_personal_profile,
+ 'job-poster': self.__parse_personal_profile,
+ 'update-comments': self.__parse_update_comments,
+ 'connections': self.__parse_connections,
+ 'error': self.__parse_error,
+ 'position': self.__parse_position,
+ 'education': self.__parse_education,
+ 'people': self.__parse_people_collection
+ }
+ self.tree = etree.fromstring(content)
+ self.root = self.tree.tag
+ self.results = self.__forward_tree(self.tree, self.root)
+
+ def __forward_tree(self, tree, root):
+ results = self.routing[root](tree)
+ return results
+
+ def __parse_network_updates(self, tree):
+ content = LinkedInNetworkUpdateParser(tree).results
+ return content
+
+ def __parse_personal_profile(self, tree):
+ content = LinkedInProfileParser(tree).results
+ return content
+
+ def __parse_update_comments(self, tree):
+ content = LinkedInNetworkCommentParser(tree).results
+ return content
+
+ def __parse_connections(self, tree):
+ content = LinkedInConnectionsParser(tree).results
+ return content
+
+ def __parse_error(self, tree):
+ content = LinkedInErrorParser(tree).results
+ return content
+
+ def __parse_position(self, tree):
+ content = LinkedInPositionParser(tree).results
+ return content
+
+ def __parse_education(self, tree):
+ content = LinkedInEducationParser(tree).results
+ return content
+
+ def __parse_people_collection(self, tree):
+ ppl = tree.getchildren()
+ content = []
+ for p in ppl:
+ rslts = LinkedInProfileParser(p).results
+ content.append(rslts)
+ return content
+
+class LinkedInNetworkUpdateParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.xpath_collection = {
+ 'first-name': etree.XPath('update-content/person/first-name'),
+ 'profile-url': etree.XPath('update-content/person/site-standard-profile-request/url'),
+ 'last-name': etree.XPath('update-content/person/last-name'),
+ 'timestamp': etree.XPath('timestamp'),
+ 'updates': etree.XPath('updates'),
+ 'update': etree.XPath('updates/update'),
+ 'update-type': etree.XPath('update-type'),
+ 'update-key': etree.XPath('update-key'),
+ #special paths for question/answer updates
+ 'qa-first-name': etree.XPath('update-content/question/author/first-name'),
+ 'qa-last-name': etree.XPath('update-content/question/author/last-name'),
+ 'qa-profile-url': etree.XPath('update-content/question/web-url'),
+ 'jobp-title': etree.XPath('update-content/job/position/title'),
+ 'jobp-company': etree.XPath('update-content/job/company/name'),
+ 'jobp-url': etree.XPath('update-content/job/site-job-request/url')
+ }
+ self.tree = content
+ total = self.xpath_collection['updates'](self.tree)[0].attrib['total']
+ self.results = self.__build_data(self.tree, total)
+
+ def __build_data(self, tree, total):
+ results = {}
+ objs = []
+ results['total'] = total
+ updates = self.xpath_collection['update'](tree)
+ for u in updates:
+ types = self.xpath_collection['update-type'](u)[0].text
+ if types == 'QSTN' or types == 'ANSW':
+ data = self.__qa_data_builder(u)
+ elif types == 'JOBP':
+ data = self.__jobp_data_builder(u)
+ else:
+ data = self.__generic_data_builder(u)
+ obj = self.__objectify(data, types, u)
+ objs.append(obj)
+ results['results'] = objs
+ return results
+
+ def __generic_data_builder(self, u):
+ data = {}
+ try:
+ data['update_key'] = self.xpath_collection['update-key'](u)[0].text.strip()
+ except IndexError:
+ pass
+ data['first_name'] = self.xpath_collection['first-name'](u)[0].text.strip()
+ data['profile_url'] = self.xpath_collection['profile-url'](u)[0].text.strip()
+ data['last_name'] = self.xpath_collection['last-name'](u)[0].text.strip()
+ data['timestamp'] = self.xpath_collection['timestamp'](u)[0].text.strip()
+ return data
+
+ def __qa_data_builder(self, u):
+ data = {}
+ data['first_name'] = self.xpath_collection['qa-first-name'](u)[0].text.strip()
+ try:
+ data['profile_url'] = self.xpath_collection['qa-profile-url'](u)[0].text.strip()
+ except IndexError: #the answers url is in a different spot, that's handled by the object
+ pass
+ data['last_name'] = self.xpath_collection['qa-last-name'](u)[0].text.strip()
+ data['timestamp'] = self.xpath_collection['timestamp'](u)[0].text.strip()
+ return data
+
+ def __jobp_data_builder(self, u):
+ data = {}
+ data['job_title'] = self.xpath_collection['jobp-title'](u)[0].text.strip()
+ data['job_company'] = self.xpath_collection['jobp-company'](u)[0].text.strip()
+ data['profile_url'] = self.xpath_collection['jobp-url'](u)[0].text.strip()
+ return data
+
+ def __objectify(self, data, u_type, u):
+ if u_type == 'STAT':
+ obj = mappers.NetworkStatusUpdate(data, u)
+ elif u_type == 'CONN':
+ obj = mappers.NetworkConnectionUpdate(data, u)
+ elif u_type == 'JGRP':
+ obj = mappers.NetworkGroupUpdate(data, u)
+ elif u_type == 'NCON':
+ obj = mappers.NetworkNewConnectionUpdate(data, u)
+ elif u_type == 'CCEM':
+ obj = mappers.NetworkAddressBookUpdate(data, u)
+ elif u_type == 'QSTN':
+ obj = mappers.NetworkQuestionUpdate(data, u)
+ elif u_type == 'ANSW':
+ obj = mappers.NetworkAnswerUpdate(data, u)
+ elif u_type == 'JOBP':
+ obj = mappers.NetworkJobPostingUpdate(data, u)
+ return obj
+
+class LinkedInProfileParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.tree = content
+ self.results = self.__build_data(self.tree)
+
+ def __build_data(self, tree):
+ results = []
+ for p in tree.xpath('/person'):
+ person = {}
+ for item in p.getchildren():
+ if item.tag == 'location':
+ person['location'] = item.getchildren()[0].text
+ else:
+ person[re.sub(r'-', '_', item.tag)] = item.text
+ obj = mappers.Profile(person, p)
+ results.append(obj)
+ if not results:
+ person = {}
+ for item in tree.getchildren():
+ person[re.sub(r'-', '_', item.tag)] = item.text
+ obj = mappers.Profile(person, tree)
+ results.append(obj)
+ return results
+
+class LinkedInNetworkCommentParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.tree = content
+ self.comment_xpath = etree.XPath('update-comment')
+ self.results = self.__build_data(self.tree)
+
+ def __build_data(self, tree):
+ if not tree.getchildren():
+ return []
+ else:
+ objs = []
+ for c in self.comment_xpath(tree):
+ obj = mappers.NetworkUpdateComment(c)
+ objs.append(obj)
+ return objs
+
+class LinkedInConnectionsParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.tree = content
+ self.total = content.attrib['total']
+ self.results = self.__build_data(self.tree)
+
+ def __build_data(self, tree):
+ results = {}
+ results['results'] = []
+ for p in tree.getchildren():
+ parsed = LinkedInXMLParser(etree.tostring(p)).results[0]
+ results['results'].append(parsed)
+ results['total'] = self.total
+ return results
+
+class LinkedInErrorParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.tree = content
+ self.xpath_collection = {
+ 'status': etree.XPath('status'),
+ 'timestamp': etree.XPath('timestamp'),
+ 'error-code': etree.XPath('error-code'),
+ 'message': etree.XPath('message')
+ }
+ self.results = self.__build_data(self.tree)
+
+ def __build_data(self, tree):
+ data = {}
+ data['status'] = self.xpath_collection['status'](tree)[0].text.strip()
+ data['timestamp'] = self.xpath_collection['timestamp'](tree)[0].text.strip()
+ data['error_code'] = self.xpath_collection['error-code'](tree)[0].text.strip()
+ data['message'] = self.xpath_collection['message'](tree)[0].text.strip()
+ results = mappers.LinkedInError(data, tree)
+ return results
+
+class LinkedInPositionParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.tree = content
+ self.xpath_collection = {
+ 'id': etree.XPath('id'),
+ 'title': etree.XPath('title'),
+ 'summary': etree.XPath('summary'),
+ 'start-date': etree.XPath('start-date'),
+ 'end-date': etree.XPath('end-date'),
+ 'is-current': etree.XPath('is-current'),
+ 'company': etree.XPath('company/name')
+ }
+ self.results = self.__build_data(self.tree)
+
+ def __build_data(self, tree):
+ data = {}
+ try:
+ data['id'] = self.xpath_collection['id'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except:
+ data['id'] = None
+ try:
+ data['title'] = self.xpath_collection['title'](tree)[0].text.strip() \
+ if len(self.xpath_collection['title'](tree)) else None
+ except:
+ data['title'] = None
+ try:
+ data['summary'] = self.xpath_collection['summary'](tree)[0].text.strip() \
+ if len(self.xpath_collection['summary'](tree)) else None
+ except:
+ data['summary'] = None
+ try:
+ data['start_date'] = self.xpath_collection['start-date'](tree)[0].text.strip() \
+ if len(self.xpath_collection['start-date'](tree)) else None
+ except:
+ data['start_date'] = None
+ try:
+ data['end_date'] = self.xpath_collection['end-date'](tree)[0].text.strip() \
+ if len(self.xpath_collection['end-date'](tree)) else None
+ except:
+ data['end_date'] = None
+ try:
+ data['is_current'] = self.xpath_collection['is-current'](tree)[0].text.strip() \
+ if len(self.xpath_collection['is-current'](tree)) else None
+ except:
+ data['is_current'] = None
+ try:
+ data['company'] = self.xpath_collection['company'](tree)[0].text.strip() \
+ if len(self.xpath_collection['company'](tree)) else None
+ except:
+ data['company'] = None
+ results = mappers.Position(data, tree)
+ return results
+
+class LinkedInEducationParser(LinkedInXMLParser):
+ def __init__(self, content):
+ self.tree = content
+ self.xpath_collection = {
+ 'id': etree.XPath('id'),
+ 'school-name': etree.XPath('school-name'),
+ 'field-of-study': etree.XPath('field-of-study'),
+ 'start-date': etree.XPath('start-date/year'),
+ 'end-date': etree.XPath('end-date/year'),
+ 'degree': etree.XPath('degree'),
+ 'activities': etree.XPath('activities')
+ }
+ self.results = self.__build_data(self.tree)
+
+ def __build_data(self, tree):
+ data = {}
+ try:
+ data['id'] = self.xpath_collection['id'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except Exception, e:
+ print e
+ data['id'] = None
+ try:
+ data['school_name'] = self.xpath_collection['school-name'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except Exception, e:
+ print e
+ data['school_name'] = None
+ try:
+ data['field_of_study'] = self.xpath_collection['field-of-study'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except:
+ data['field_of_study'] = None
+ try:
+ data['start_date'] = self.xpath_collection['start-date'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except:
+ data['start_date'] = None
+ try:
+ data['end_date'] = self.xpath_collection['end-date'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except:
+ data['end_date'] = None
+ try:
+ data['degree'] = self.xpath_collection['degree'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except:
+ data['degree'] = None
+ try:
+ data['activities'] = self.xpath_collection['activities'](tree)[0].text.strip() \
+ if len(self.xpath_collection['id'](tree)) else None
+ except:
+ data['activities'] = None
+ results = mappers.Education(data, tree)
+ return results
View
BIN liclient/parsers/lixml.pyc
Binary file not shown.
View
253 liclient/parsers/mappers.py
@@ -0,0 +1,253 @@
+from lxml import etree
+import datetime, re
+import lixml
+
+class LinkedInData(object):
+ def __init__(self, data, xml):
+ self.xml = xml
+ self.parse_data(data)
+
+ def parse_data(self, data):
+ for k in data.keys():
+ self.__dict__[k] = data[k]
+
+ def jsonify(self):
+ json = {}
+ for k in self.__dict__.keys():
+ if type(self.__dict__[k]) == type(''):
+ json[k] = self.__dict__[k]
+ return json
+
+ def xmlify(self):
+ converted = [re.sub('_', '-', k) for k in self.__dict__.keys()]
+ for d in self.xml.iter(tag=etree.Element):
+ if d.tag in converted:
+ try:
+ d.text = self.__dict__[re.sub('-', '_', d.tag)]
+ except:
+ continue
+ return etree.tostring(self.xml)
+
+ def __str__(self):
+ return self.update_content if self.update_content else '<No content>'
+
+class LinkedInError(LinkedInData):
+ def __repr__(self):
+ return '<LinkedIn Error code %s>'.encode('utf-8') % self.status
+
+class NetworkUpdate(LinkedInData):
+ def __init__(self, data, xml):
+ self.xml = xml
+ self.update_key = None
+ self.parse_data(data)
+
+ def jsonify(self):
+ jsondict = {'first_name': self.first_name,
+ 'last_name': self.last_name,
+ 'update_content': self.update_content,
+ 'timestamp': self.timestamp,
+ 'update_key': self.update_key,
+ 'profile_url': self.profile_url}
+ return jsondict
+
+class NetworkStatusUpdate(NetworkUpdate):
+ def __init__(self, data, xml):
+ self.status_xpath = etree.XPath('update-content/person/current-status')
+ self.comment_xpath = etree.XPath('update-comments/update-comment')
+ self.update_key = None
+ self.xml = xml
+ self.parse_data(data)
+ self.update_content = self.status_xpath(xml)[0].text.strip()
+ self.comments = []
+ self.get_comments()
+
+ def get_comments(self):
+ for c in self.comment_xpath(self.xml):
+ comment = NetworkUpdateComment(c)
+ self.comments.append(comment)
+ return
+
+class NetworkConnectionUpdate(NetworkUpdate):
+ def __init__(self, data, xml):
+ self.xml = xml
+ self.update_key = None
+ self.parse_data(data)
+ self.connection_target = etree.XPath('update-content/person/connections/person')
+ self.targets = []
+ self.get_targets()
+ self.set_update_content(self.targets)
+
+ def get_targets(self):
+ for p in self.connection_target(self.xml):
+ obj = lixml.LinkedInProfileParser(p).results
+ self.targets = obj
+
+ def set_update_content(self, targets):
+ update_str = self.first_name + ' ' + self.last_name + ' is now connected with '
+ if len(targets) == 1:
+ update_str += targets[0].first_name + ' ' + targets[0].last_name
+ else:
+ for t in targets:
+ update_str += t.first_name + ' ' + t.last_name + ', and '
+ update_str = re.sub(', and $', '', update_str)
+ self.update_content = update_str
+ return
+
+class NetworkNewConnectionUpdate(NetworkConnectionUpdate):
+ def get_targets(self):
+ self.connection_target = etree.XPath('update-content/person/')
+ for p in self.connection_target(self.xml):
+ obj = LinkedInProfileParser(p).results
+ self.targets = obj
+
+ def set_update_content(self, target):
+ update_str = ' is now connected with you.'
+ update_str = targets[0].first_name + ' ' + targets[0].last_name + update_str
+ self.update_content = update_str
+ return
+
+class NetworkAddressBookUpdate(NetworkNewConnectionUpdate):
+ def set_update_content(self, target):
+ update_str = ' just joined LinkedIn.'
+ update_str = self.targets[0].first_name + ' ' + self.targets[0].last_name + update_str
+ self.update_content = update_str
+ return
+
+class NetworkGroupUpdate(NetworkUpdate):
+ def __init__(self, data, xml):
+ self.update_key = None
+ self.xml = xml
+ self.parse_data(data)
+ self.group_target = etree.XPath('update-content/person/member-groups/member-group')
+ self.group_name_target = etree.XPath('name')
+ self.group_url_target = etree.XPath('site-group-request/url')
+ self.targets = []
+ self.get_targets()
+ self.set_update_content(self.targets)
+
+ def get_targets(self):
+ for g in self.group_target(self.xml):
+ target_dict = {}
+ k = self.group_name_target(g)[0].text.strip()
+ v = self.group_url_target(g)[0].text.strip()
+ target_dict[k] = v
+ self.targets.append(target_dict)
+ return
+
+ def set_update_content(self, targets):
+ update_str = self.first_name + ' ' + self.last_name + ' joined '
+ if len(targets) == 1:
+ update_str += '<a href="'+targets[0].values()[0]+'">'+targets[0].keys()[0] + '</a>'
+ else:
+ for t in targets:
+ update_str += '<a href="'+t.values()[0]+'">'+t.keys()[0] + '</a>, and '
+ update_str = re.sub(', and $', '', update_str)
+ self.update_content = update_str
+ return
+
+class NetworkQuestionUpdate(NetworkUpdate):
+ def __init__(self, data, xml):
+ self.xml = xml
+ self.update_key = None
+ self.parse_data(data)
+ self.question_title_xpath = etree.XPath('update-content/question/title')
+ self.set_update_content()
+
+ def set_update_content(self):
+ update_str = self.first_name + ' ' + self.last_name + ' asked a question: '
+ qstn_text = self.question_title_xpath(self.xml)[0].text.strip()
+ update_str += qstn_text
+ self.update_content = update_str
+ return
+
+class NetworkAnswerUpdate(NetworkUpdate):
+ def __init__(self, data, xml):
+ self.update_key = None
+ self.xml = xml
+ self.parse_data(data)
+ self.question_title_xpath = etree.XPath('update-content/question/title')
+ self.answer_xpath = etree.XPath('update-content/question/answers/answer')
+ self.get_answers()
+ self.set_update_content()
+
+ def get_answers(self):
+ for a in self.answer_xpath(self.xml):
+ self.profile_url = a.xpath('web-url')[0].text.strip()
+ self.first_name = a.xpath('author/first-name')[0].text.strip()
+ self.last_name = a.xpath('author/last-name')[0].text.strip()
+
+ def set_update_content(self):
+ update_str = self.first_name + ' ' + self.last_name + ' answered: '
+ qstn_text = self.question_title_xpath(self.xml)[0].text.strip()
+ update_str += qstn_text
+ self.update_content = update_str
+ return
+
+class NetworkJobPostingUpdate(NetworkUpdate):
+ def __init__(self, data, xml):
+ self.xml = xml
+ self.parse_data(data)
+ self.set_update_content()
+ self.poster = lixml.LinkedInXMLParser(xml.xpath('job-poster')[0])
+
+ def set_update_content(self):
+ update_str = self.poster.first_name + ' ' + self.poster.last_name + ' posted a job: ' + self.job_title
+ self.update_content = update_str
+ return
+
+class NetworkUpdateComment(LinkedInData):
+ def __init__(self, xml):
+ self.xml = xml
+ self.comment_xpath = etree.XPath('comment')
+ self.person_xpath = etree.XPath('person')
+ self.__content = lixml.LinkedInXMLParser(etree.tostring(self.person_xpath(xml)[0])).results[0]
+ self.first_name = self.__content.first_name
+ self.last_name = self.__content.last_name
+ self.profile_url = self.__content.profile_url
+ self.update_content = self.comment_xpath(xml)[0].text
+
+ def jsonify(self):
+ jsondict = {'first_name': self.first_name,
+ 'last_name': self.last_name,
+ 'update_content': self.update_content,
+ 'profile_url': self.profile_url}
+ return jsondict
+
+class Profile(LinkedInData):
+ def __init__(self, data, xml):
+ self.profile_url = ''
+ self.xml = xml
+ self.parse_data(data)
+ self.positions = []
+ self.educations = []
+ if not self.profile_url:
+ self.set_profile_url()
+ self.get_positions()
+ self.get_educations()
+
+ def set_profile_url(self):
+ try:
+ profile_url_xpath = etree.XPath('site-standard-profile-request/url')
+ self.profile_url = profile_url_xpath(self.xml)[0].text.strip()
+ except:
+ pass
+
+ def get_positions(self):
+ profile_position_xpath = etree.XPath('positions/position')
+ pos = profile_position_xpath(self.xml)
+ for p in pos:
+ obj = lixml.LinkedInXMLParser(etree.tostring(p)).results
+ self.positions.append(obj)
+
+ def get_educations(self):
+ profile_education_xpath = etree.XPath('educations/education')
+ eds = profile_education_xpath(self.xml)
+ for e in eds:
+ obj = lixml.LinkedInXMLParser(etree.tostring(e)).results
+ self.educations.append(obj)
+
+class Position(LinkedInData):
+ pass
+
+class Education(LinkedInData):
+ pass
View
BIN liclient/parsers/mappers.pyc
Binary file not shown.
View
BIN liclient/parsers/xml.pyc
Binary file not shown.
View
2 setup.py
@@ -9,5 +9,5 @@
description='Library for accessing the LinkedIn API in Python',
packages=['liclient', 'liclient.parsers', 'liclient.oauth', 'liclient.analysis'],
license='MIT',
- package_data='*.txt'
+ package_data={'':['*.txt']}
)
View
13 setup.py~
@@ -0,0 +1,13 @@
+from distutils.core import setup
+
+setup(
+ name='LinkedIn Client Library',
+ version='1.0',
+ author='Aaron Brenzel',
+ author_email='abrenzel@millerresource.com',
+ url='12.236.169.60:4830/liclient',
+ description='Library for accessing the LinkedIn API in Python',
+ packages=['liclient', 'liclient.parsers', 'liclient.oauth'],
+ license='MIT',
+ package_data='*.txt'
+)

0 comments on commit 00cdf4d

Please sign in to comment.