def test_multiple_parses_are_idempotent(self): url = u'http://xn-hllo-bpa.com/path%20withspaces?query=es%25capes&foo=bar#frag%28withescapes%29' parse1 = URLObject.parse(url) self.assertEqual(unicode(url), unicode(parse1)) parse2 = URLObject.parse(unicode(parse1)) self.assertEqual(unicode(url), unicode(parse2)) self.assertEqual(unicode(parse1), unicode(parse2))
def access_token_url(self): """The URL to retrieve to exchange a code for an access token.""" url = URLObject.parse('https://graph.facebook.com/oauth/access_token') url |= ('code', self.request.GET['code']) url |= ('client_id', self.client_id()) url |= ('client_secret', self.client_secret()) url |= ('redirect_uri', self.redirect_uri()) return unicode(url)
def url_with_page_number(self, page_number): """ Constructs a url used for getting the next/previous urls """ url = URLObject.parse(self.request.get_full_path()) url = url.add_query_param('page', page_number) limit = self.get_limit() if limit != self.limit: url = url.add_query_param('limit', limit) return url
def authorize_url(self): """The URL to redirect the client to for authorization.""" url = URLObject.parse('https://graph.facebook.com/oauth/authorize') url |= ('client_id', self.client_id()) url |= ('redirect_uri', self.redirect_uri()) scope = self.scope() if scope: url |= ('scope', ','.join(scope)) display = self.display() if display: url |= ('display', display) return url
def __init__(self, url, port): self.url = URL.parse(url).with_port(port)
def setUp(self): self.url = URLObject.parse(u'http://www.google.com/search?q=something&hl=en#frag')
def test_host_idna_encoding_is_parsed(self): url = URLObject.parse(u'http://xn--hllo-bpa.com/') self.assertEqual(url.host, u'héllo.com')
def test_query_is_not_double_escaped(self): url = URLObject.parse('http://www.google.com/search?q=a%20string%20with%20escapes') self.assertEqual(unicode(url), 'http://www.google.com/search?q=a%20string%20with%20escapes') self.assertEqual(url.query, 'q=a%20string%20with%20escapes')
def url(self): return URLObject.parse(self.BASE_URL)
def test_path_is_not_double_escaped(self): url = URLObject.parse('http://www.google.com/path%20with%20spaces') self.assertEqual(unicode(url), 'http://www.google.com/path%20with%20spaces') self.assertEqual(url.path, '/path with spaces')
def test_fragment_is_not_double_escaped(self): url = URLObject.parse('http://google.com/#frag%20with%20escapes') self.assertEqual(unicode(url), 'http://google.com/#frag%20with%20escapes') self.assertEqual(url.fragment, 'frag with escapes')
class FQL(object): """ A maker of single and multiple FQL queries. Usage ===== Single queries: >>> q = FQL('access_token') >>> result = q("SELECT post_id FROM stream WHERE source_id = ...") >>> result [Bunch(post_id='XXXYYYZZZ'), ...] >>> result[0] Bunch(post_id='XXXYYYZZZ') >>> result[0].post_id 'XXXYYYZZZ' Multiple queries: >>> q = FQL('access_token') >>> result = q.multi(dict(query1="SELECT...", query2="SELECT...")) >>> result[0].name 'query1' >>> result[0].fql_result_set [...] >>> result[1].name 'query2' >>> result[1].fql_result_set [...] """ ENDPOINT = URLObject.parse('https://api.facebook.com/method/') def __init__(self, access_token=None, err_handler=None): self.access_token = access_token self.err_handler = err_handler def __call__(self, query, **params): """ Execute a single FQL query (using `fql.query`). Example: >>> q = FQL('access_token') >>> result = q("SELECT post_id FROM stream WHERE source_id = ...") >>> result [Bunch(post_id='XXXYYYZZZ'), ...] >>> result[0] Bunch(post_id='XXXYYYZZZ') >>> result[0].post_id 'XXXYYYZZZ' """ url = self.ENDPOINT / 'fql.query' params.update(query=query, access_token=self.access_token, format='json') url |= params return self.fetch_json(url) def multi(self, queries, **params): """ Execute multiple FQL queries (using `fql.multiquery`). Example: >>> q = FQL('access_token') >>> result = q.multi(dict(query1="SELECT...", query2="SELECT...")) >>> result[0].name 'query1' >>> result[0].fql_result_set [...] >>> result[1].name 'query2' >>> result[1].fql_result_set [...] """ url = self.ENDPOINT / 'fql.multiquery' params.update(queries=json.dumps(queries), access_token=self.access_token, format='json') url |= params return self.fetch_json(url) def fetch_json(self, url, data=None): response = json.loads(self.__class__.fetch(url, data=data)) if isinstance(response, dict): if response.get("error_msg"): code = response.get("error_code") msg = response.get("error_msg") args = response.get("request_args") e = GraphException(code, msg, args=args) if self.err_handler: self.err_handler(e) else: raise e return bunch.bunchify(response) @staticmethod def fetch(url, data=None): conn = urllib2.urlopen(url, data=data) try: return conn.read() finally: conn.close()
def test_host_idna_encoding_is_preserved(self): url = URLObject.parse(u'http://xn--hllo-bpa.com/') self.assertEqual(unicode(url), u'http://xn--hllo-bpa.com/')
class Graph(object): """ Proxy for accessing the Facebook Graph API. This class uses dynamic attribute handling to provide a flexible and future-proof interface to the Graph API. Tutorial ======== To get started using the API, create a new `Graph` instance with an access token: >>> g = Graph(access_token) # Access token is optional. >>> g <Graph('https://graph.facebook.com/') at 0x...> Addressing Nodes ---------------- Each `Graph` contains an access token and a URL. The graph you just created will have its URL set to 'https://graph.facebook.com/' by default (this is defined as the class attribute `Graph.API_ROOT`). The URL is represented as a `URLObject`; see <http://github.com/zacharyvoase/urlobject> for more information. Remember that you can treat it exactly as you would a `unicode` string; it just supports a few more methods to enable easy URL manipulation. >>> g.url <URLObject(u'https://graph.facebook.com/') at 0x...> >>> print g.url https://graph.facebook.com/ To address child nodes within the Graph API, `Graph` supports dynamic attribute and item lookups: >>> g.me <Graph('https://graph.facebook.com/me') at 0x...> >>> g.me.home <Graph('https://graph.facebook.com/me/home') at 0x...> >>> g['me']['home'] <Graph('https://graph.facebook.com/me/home') at 0x...> >>> g[123456789] <Graph('https://graph.facebook.com/123456789') at 0x...> Note that a `Graph` instance is rarely modified; these methods return copies of the original object. In addition, the API is lazy: HTTP requests will never be made unless you explicitly make them. Retrieving Nodes ---------------- You can fetch data by calling a `Graph` instance: >>> about_me = g.me() >>> about_me Node({'about': '...', 'id': '1503223370'}) This returns a `Node` object, which contains the retrieved data. `Node` is a subclass of `bunch.Bunch`, so you can access keys using attribute syntax: >>> about_me.id '1503223370' >>> about_me.first_name 'Zachary' >>> about_me.hometown.name 'London, United Kingdom' Accessing non-existent attributes or items will return a `Graph` instance corresponding to a child node. This `Graph` can then be called normally, to retrieve the child node it represents: >>> about_me.home <Graph('https://graph.facebook.com/me/home') at 0x...> >>> about_me.home() Node({'data': [...]}) See `Node`’s documentation for further examples. Creating, Updating and Deleting Nodes ------------------------------------- With the Graph API, node manipulation is done via HTTP POST requests. The `post()` method on `Graph` instances will POST to the current URL, with varying semantics for each endpoint: >>> post = g.me.feed.post(message="Test.") # Status update >>> post Node({'id': '...'}) >>> g[post.id].comments.post(message="A comment.") # Comment on the post Node({'id': '...'}) >>> g[post.id].likes.post() # Like the post True >>> event = g[121481007877204]() >>> event.name 'Facebook Developer Garage London May 2010' >>> event.rsvp_status is None True >>> event.attending.post() # Attend the given event True Deletes are just POST requests with `?method=delete`; the `delete()` method is a helpful shortcut: >>> g[post.id].delete() True """ API_ROOT = URLObject.parse('https://graph.facebook.com/') DEFAULT_TIMEOUT = 0 # No timeout as default def __init__(self, access_token=None, err_handler=None, timeout=DEFAULT_TIMEOUT, retries=5, urllib2=None, httplib=None, **state): self.access_token = access_token self.err_handler = err_handler self.url = self.API_ROOT self.timeout = timeout self.retries = retries self.__dict__.update(state) if urllib2 is None: import urllib2 self.urllib2 = urllib2 if httplib is None: import httplib self.httplib = httplib def __repr__(self): return '<Graph(%r) at 0x%x>' % (str(self.url), id(self)) def copy(self, **update): """Copy this Graph, optionally overriding some attributes.""" return type(self)(access_token=self.access_token, err_handler=self.err_handler, timeout=self.timeout, retries=self.retries, urllib2=self.urllib2, httplib=self.httplib, **update) def __getitem__(self, item): if isinstance(item, slice): params = {'offset': item.start, 'limit': item.stop - item.start} return self.copy(url=(self.url & params)) return self.copy(url=(self.url / unicode(item))) def __getattr__(self, attr): return self[attr] def __or__(self, params): return self.copy(url=(self.url | params)) def __and__(self, params): return self.copy(url=(self.url & params)) def __call__(self, **params): """Read the current URL, and JSON-decode the results.""" if self.access_token: params['access_token'] = self.access_token data = json.loads( self.fetch(self.url | params, timeout=self.timeout, retries=self.retries, urllib2=self.urllib2, httplib=self.httplib)) return self.node(data, params) def __iter__(self): raise TypeError('%r object is not iterable' % self.__class__.__name__) def __sentry__(self): return 'Graph(url: %s, params: %s)' % (self.url, repr(self.__dict__)) def fields(self, *fields): """Shortcut for `?fields=x,y,z`.""" return self | ('fields', ','.join(fields)) def ids(self, *ids): """Shortcut for `?ids=1,2,3`.""" return self | ('ids', ','.join(map(str, ids))) def node(self, data, params, method=None): return Node._new(self, data, err_handler=self.err_handler, params=params, method=method) def post(self, **params): """ POST to this URL (with parameters); return the JSON-decoded result. Example: >>> Graph('ACCESS TOKEN').me.feed.post(message="Test.") Node({'id': '...'}) Some methods allow file attachments so uses MIME request to send those through. Must pass in a file object as 'file' """ if self.access_token: params['access_token'] = self.access_token if self.url.path.split('/')[-1] in ['photos']: params['timeout'] = self.timeout params['httplib'] = self.httplib fetch = partial(self.post_mime, self.url, httplib=self.httplib, retries=self.retries, **params) else: params = dict([(k, v.encode('UTF-8')) for (k, v) in params.iteritems() if v is not None]) fetch = partial(self.fetch, self.url, urllib2=self.urllib2, httplib=self.httplib, timeout=self.timeout, retries=self.retries, data=urllib.urlencode(params)) data = json.loads(fetch()) return self.node(data, params, "post") def post_file(self, file, **params): if self.access_token: params['access_token'] = self.access_token params['file'] = file params['timeout'] = self.timeout params['httplib'] = self.httplib data = json.loads(self.post_mime(self.url, **params)) return self.node(data, params, "post_file") @staticmethod def post_mime(url, httplib=default_httplib, timeout=DEFAULT_TIMEOUT, retries=5, **kwargs): body = [] crlf = '\r\n' boundary = "graphBoundary" # UTF8 params utf8_kwargs = dict([(k, v.encode('UTF-8')) for (k, v) in kwargs.iteritems() if k != 'file' and v is not None]) # Add args for (k, v) in utf8_kwargs.iteritems(): body.append("--" + boundary) body.append('Content-Disposition: form-data; name="%s"' % k) body.append('') body.append(str(v)) # Add raw data file = kwargs.get('file') if file: file.open() data = file.read() file.close() body.append("--" + boundary) body.append( 'Content-Disposition: form-data; filename="facegraphfile.png"') body.append('') body.append(data) body.append("--" + boundary + "--") body.append('') body = crlf.join(body) # Post to server kwargs = {} if timeout: kwargs = {'timeout': timeout} r = httplib.HTTPSConnection(url.host, **kwargs) headers = { 'Content-Type': 'multipart/form-data; boundary=%s' % boundary, 'Content-Length': str(len(body)), 'MIME-Version': '1.0' } r.request('POST', url.path.encode(), body, headers) attempt = 0 while True: try: return r.getresponse().read() except (httplib.BadStatusLine, IOError): if attempt < retries: attempt += 1 else: raise finally: r.close() def delete(self): """Delete this resource. Sends a POST with `?method=delete`.""" return self.post(method='delete') @staticmethod def fetch(url, data=None, urllib2=default_urllib2, httplib=default_httplib, timeout=DEFAULT_TIMEOUT, retries=None): """ Fetch the specified URL, with optional form data; return a string. This method exists mainly for dependency injection purposes. By default it uses urllib2; you may override it and use an alternative library. """ conn = None attempt = 0 while True: try: kwargs = {} if timeout: kwargs = {'timeout': timeout} conn = urllib2.urlopen(url, data=data, **kwargs) return conn.read() except urllib2.HTTPError, e: return e.fp.read() except (httplib.BadStatusLine, IOError): if attempt < retries: attempt += 1 else: raise finally:
def add_query_param(url, param): (key, sep, val) = param.partition('=') return unicode(URLObject.parse(url) & (key, val))
def previous_page(self): """Shortcut for a `Graph` pointing to the previous page.""" return self._api.copy(url=URLObject.parse(self.paging.next))
def render(self, context): kwargs = MultiValueDict() for key in self.kwargs: key = smart_str(key, 'ascii') values = [value.resolve(context) for value in self.kwargs.getlist(key)] kwargs.setlist(key, values) if 'base' in kwargs: url = URLObject.parse(kwargs['base']) else: url = URLObject(scheme='http') if 'secure' in kwargs: if convert_to_boolean(kwargs['secure']): url = url.with_scheme('https') else: url = url.with_scheme('http') if 'query' in kwargs: query = kwargs['query'] if isinstance(query, basestring): query = render_template_from_string_without_autoescape(query, context) url = url.with_query(query) if 'add_query' in kwargs: for query_to_add in kwargs.getlist('add_query'): if isinstance(query_to_add, basestring): query_to_add = render_template_from_string_without_autoescape(query_to_add, context) query_to_add = dict(decode_query(query_to_add)) for key, value in query_to_add.items(): url = url.add_query_param(key, value) if 'scheme' in kwargs: url = url.with_scheme(kwargs['scheme']) if 'host' in kwargs: url = url.with_host(kwargs['host']) if 'path' in kwargs: url = url.with_path(kwargs['path']) if 'add_path' in kwargs: for path_to_add in kwargs.getlist('add_path'): url = url.add_path_component(path_to_add) if 'fragment' in kwargs: url = url.with_fragment(kwargs['fragment']) if 'port' in kwargs: url = url.with_port(kwargs['port']) # sensible default if not url.host: url = url.with_scheme('') # Convert the URLObject to its unicode representation url = unicode(url) # Handle escaping. By default, use the value of # context.autoescape. This can be overridden by # passing an "autoescape" keyword to the tag. if 'autoescape' in kwargs: autoescape = convert_to_boolean(kwargs['autoescape']) else: autoescape = context.autoescape if autoescape: url = escape(url) if self.asvar: context[self.asvar] = url return '' return url